/** * @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 */ 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 - 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; }