Files
claude-os/apps/sh/sh.c
AI 5229758092 Add FAT32 driver, mount app, fix shell cd/path resolution
- FAT32 VFS driver (fat32.h, fat32.c): reads BPB, FAT table, cluster
  chains, directory entries (8.3 SFN), file read/write, mount via
  fat32_mount_device()
- mount app: writes 'device mountpoint' to /sys/vfs/mount to trigger
  FAT32 mount from userspace
- VFS sysfs mount namespace in kernel.c: handles mount requests via
  sysfs write to /sys/vfs/mount, delegates to FAT32 driver
- Shell cd: validates target directory exists before updating CWD,
  resolves relative paths (., .., components) to absolute paths
- Shell run_command: resolves ARG1 paths relative to CWD so apps
  like cat and ls receive absolute paths automatically
- Fixed FAT32 root directory access: use fs->root_cluster when
  VFS mount root node has inode=0
2026-02-23 14:52:41 +00:00

366 lines
10 KiB
C

/**
* @file sh.c
* @brief ClaudeOS shell.
*
* A simple interactive shell that reads commands from stdin,
* supports built-in commands (cd, exit, help, env), and
* executes external programs via fork+exec.
*/
#include "syscalls.h"
/** Maximum command line length. */
#define CMD_MAX 256
/** Read a line from stdin with echo and basic line editing.
* Returns length of the line (excluding newline). */
static int readline(char *buf, int maxlen) {
int pos = 0;
while (pos < maxlen - 1) {
char c;
int n = read(0, &c, 1);
if (n <= 0) {
/* No data yet: yield CPU and retry */
yield();
continue;
}
if (c == '\n' || c == '\r') {
putchar('\n');
break;
} else if (c == '\b' || c == 127) {
/* Backspace */
if (pos > 0) {
pos--;
puts("\b \b"); /* Move back, overwrite with space, move back */
}
} else if (c >= 32) {
/* Printable character */
buf[pos++] = c;
putchar(c);
}
}
buf[pos] = '\0';
return pos;
}
/** Skip leading whitespace, return pointer to first non-space. */
static char *skip_spaces(char *s) {
while (*s == ' ' || *s == '\t') s++;
return s;
}
/** Find the next space or end of string. */
static char *find_space(char *s) {
while (*s && *s != ' ' && *s != '\t') s++;
return s;
}
/** String length. */
static int slen(const char *s) {
int n = 0;
while (s[n]) n++;
return n;
}
/** Resolve an absolute path from cwd + a possibly relative path.
* Handles '..', '.', and trailing slashes. Result is always absolute. */
static void resolve_path(const char *cwd, const char *arg, char *out, int out_sz) {
char tmp[256];
int ti = 0;
/* If arg is absolute, start from it; otherwise prepend cwd */
if (arg[0] == '/') {
/* copy arg into tmp */
for (int i = 0; arg[i] && ti < 255; i++)
tmp[ti++] = arg[i];
} else {
/* copy cwd */
for (int i = 0; cwd[i] && ti < 255; i++)
tmp[ti++] = cwd[i];
/* ensure separator */
if (ti > 0 && tmp[ti - 1] != '/' && ti < 255)
tmp[ti++] = '/';
/* append arg */
for (int i = 0; arg[i] && ti < 255; i++)
tmp[ti++] = arg[i];
}
tmp[ti] = '\0';
/* Now canonicalize: split on '/' and process each component */
/* Stack of component start offsets */
int comp_starts[64];
int comp_lens[64];
int depth = 0;
int i = 0;
while (tmp[i]) {
/* skip slashes */
while (tmp[i] == '/') i++;
if (!tmp[i]) break;
/* find end of component */
int start = i;
while (tmp[i] && tmp[i] != '/') i++;
int len = i - start;
if (len == 1 && tmp[start] == '.') {
/* current dir: skip */
continue;
} else if (len == 2 && tmp[start] == '.' && tmp[start + 1] == '.') {
/* parent dir: pop */
if (depth > 0) depth--;
} else {
/* normal component: push */
if (depth < 64) {
comp_starts[depth] = start;
comp_lens[depth] = len;
depth++;
}
}
}
/* Build output */
int oi = 0;
if (depth == 0) {
if (oi < out_sz - 1) out[oi++] = '/';
} else {
for (int d = 0; d < depth && oi < out_sz - 2; d++) {
out[oi++] = '/';
for (int j = 0; j < comp_lens[d] && oi < out_sz - 1; j++)
out[oi++] = tmp[comp_starts[d] + j];
}
}
out[oi] = '\0';
}
/** Built-in: cd <path> */
static void builtin_cd(char *arg) {
if (!arg || !*arg) {
arg = "/";
}
/* Get current working directory */
char cwd[128];
if (getenv("CWD", cwd, sizeof(cwd)) < 0) {
strcpy(cwd, "/");
}
/* Resolve the path (handles relative, .., etc.) */
char resolved[256];
resolve_path(cwd, arg, resolved, sizeof(resolved));
/* Validate: check if the directory exists by trying to readdir it.
* Root "/" always exists. For others, readdir index 0 must not fail
* with -1 unless the dir is simply empty, so we also accept stat-like
* checks. A simple heuristic: if readdir returns -1 for index 0 AND
* the path is not "/", we check if the *parent* can list it. */
if (resolved[0] == '/' && resolved[1] != '\0') {
/* Try listing the first entry; if the path is a valid directory,
* readdir(path, 0, ...) returns >= 0 even for an empty dir
* (it returns -1 meaning "end"), BUT if the path doesn't exist
* at all, the VFS resolve_path will fail and readdir returns -1.
*
* Better approach: try to readdir the parent and look for this
* entry among its children. */
char name[128];
int32_t type = readdir(resolved, 0, name);
/* type >= 0 means there's at least one entry => directory exists.
* type == -1 could mean empty dir or non-existent.
* For empty dirs: the directory itself was resolved successfully.
* For non-existent: the VFS resolution failed.
*
* Our VFS readdir calls resolve_path internally. If the path
* doesn't exist, it returns -1. If it exists but is empty,
* it also returns -1. We need to distinguish these.
* Use a stat-like approach: try to open the path. */
/* Actually the simplest check: try readdir on the *parent* and
* see if our target component exists as a directory entry. */
char parent[256];
char target[128];
/* Split resolved into parent + target */
int rlen = slen(resolved);
int last_slash = 0;
for (int i = 0; i < rlen; i++) {
if (resolved[i] == '/') last_slash = i;
}
if (last_slash == 0) {
/* Parent is root */
parent[0] = '/';
parent[1] = '\0';
strcpy(target, resolved + 1);
} else {
memcpy(parent, resolved, (uint32_t)last_slash);
parent[last_slash] = '\0';
strcpy(target, resolved + last_slash + 1);
}
/* Search parent directory for target */
int found = 0;
uint32_t idx = 0;
int32_t etype;
while ((etype = readdir(parent, idx, name)) >= 0) {
if (strcmp(name, target) == 0) {
if (etype == 2) {
found = 1; /* It's a directory */
} else {
puts("cd: ");
puts(resolved);
puts(": not a directory\n");
return;
}
break;
}
idx++;
}
if (!found) {
puts("cd: ");
puts(resolved);
puts(": no such directory\n");
return;
}
}
/* Update CWD environment variable */
setenv("CWD", resolved);
}
/** Built-in: env - print all known env vars. */
static void builtin_env(void) {
char buf[128];
if (getenv("CWD", buf, sizeof(buf)) >= 0) {
puts("CWD=");
puts(buf);
putchar('\n');
}
if (getenv("PATH", buf, sizeof(buf)) >= 0) {
puts("PATH=");
puts(buf);
putchar('\n');
}
if (getenv("TEST", buf, sizeof(buf)) >= 0) {
puts("TEST=");
puts(buf);
putchar('\n');
}
}
/** Built-in: help */
static void builtin_help(void) {
puts("ClaudeOS Shell\n");
puts("Built-in commands:\n");
puts(" cd <path> - change working directory\n");
puts(" env - show environment variables\n");
puts(" help - show this message\n");
puts(" exit - exit the shell\n");
puts("External commands are loaded from initrd.\n");
}
/** Check if a string looks like a path (contains / or starts with .) */
static int looks_like_path(const char *s) {
if (!s || !*s) return 0;
if (s[0] == '.' || s[0] == '/') return 1;
for (int i = 0; s[i]; i++) {
if (s[i] == '/') return 1;
}
return 0;
}
/** Execute an external command via fork+exec. */
static void run_command(const char *cmd, const char *arg) {
int32_t pid = fork();
if (pid < 0) {
puts("sh: fork failed\n");
return;
}
if (pid == 0) {
/* Child: set ARG1 if there's an argument */
if (arg && *arg) {
/* If the argument looks like a path and isn't absolute,
* resolve it relative to CWD so apps get absolute paths */
if (looks_like_path(arg) && arg[0] != '/') {
char cwd[128];
if (getenv("CWD", cwd, sizeof(cwd)) < 0) {
strcpy(cwd, "/");
}
char resolved_arg[256];
resolve_path(cwd, arg, resolved_arg, sizeof(resolved_arg));
setenv("ARG1", resolved_arg);
} else {
setenv("ARG1", arg);
}
} else {
setenv("ARG1", "");
}
/* exec the command */
int32_t ret = exec(cmd);
if (ret < 0) {
puts("sh: ");
puts(cmd);
puts(": not found\n");
exit(127);
}
/* exec doesn't return on success */
exit(1);
}
/* Parent: wait for child */
waitpid(pid);
}
int main(void) {
puts("ClaudeOS Shell v0.1\n");
puts("Type 'help' for available commands.\n\n");
char cmd[CMD_MAX];
for (;;) {
/* Print prompt with CWD */
char cwd[128];
if (getenv("CWD", cwd, sizeof(cwd)) < 0) {
strcpy(cwd, "/");
}
puts(cwd);
puts("$ ");
/* Read command */
int len = readline(cmd, CMD_MAX);
if (len == 0) continue;
/* Parse command and arguments */
char *line = skip_spaces(cmd);
if (*line == '\0') continue;
char *arg_start = find_space(line);
char *arg = (char *)0;
if (*arg_start) {
*arg_start = '\0';
arg = skip_spaces(arg_start + 1);
if (*arg == '\0') arg = (char *)0;
}
/* Check built-in commands */
if (strcmp(line, "exit") == 0) {
puts("Goodbye!\n");
exit(0);
} else if (strcmp(line, "cd") == 0) {
builtin_cd(arg);
} else if (strcmp(line, "env") == 0) {
builtin_env();
} else if (strcmp(line, "help") == 0) {
builtin_help();
} else {
/* External command */
run_command(line, arg);
}
}
return 0;
}