diff --git a/src/shell.c b/src/shell.c index 65b435a..09a2059 100644 --- a/src/shell.c +++ b/src/shell.c @@ -1331,7 +1331,68 @@ static int changeDir(const char* argument) kprintf("cd: no such directory: %s\n", path); return 1; } - vfsPath(node, cwd, sizeof(cwd)); + return 0; +} + +/* `export NAME=VALUE` -- real POSIX shell syntax, no quoting support + (matching every other built-in in this shell, e.g. doEcho below never + understands quotes either): the FIRST '=' splits `argument` into name + and value, everything after it becomes the value verbatim (including any + embedded spaces), the same "rest of the line, taken as-is" shape doEcho + already gives its own unredirected text. currentThreadSetEnv (sched.h) + sets it directly in THIS thread's own table -- the shell is a plain + kernel thread (shellRun's own comment above), so it needs no syscall to + reach its own state, exactly like changeDir calling vfsChdir directly + above. Any job spawned AFTER this call (jobctl.h, via threadCreateUser's + initialEnv) inherits a real copy of the new value. */ +/* Same "0 success, 1 failure" shape listDir/catFile/makeDir/changeDir + above use. */ +static int doExport(char* argument) +{ + char* eq = argument; + while (*eq && *eq != '=') + eq++; + if (*eq != '=' || eq == argument) { + kprintf("export: usage: export NAME=VALUE\n"); + return 1; + } + *eq = 0; + const char* value = eq + 1; + if (!currentThreadSetEnv(argument, value)) { + kprintf("export: cannot set %s (name/value too long, or too many variables)\n", argument); + return 1; + } + return 0; +} + +/* `unset NAME` -- real POSIX shell syntax, removes `argument` from this + thread's own environment table via currentThreadUnsetEnv (sched.h), + the exact same "no syscall needed, shell is a plain kernel thread" + shape as doExport above. Silently succeeds whether or not `argument` + actually had an entry (matching real unsetenv(3)'s own no-op-success + semantics for an already-absent name) -- the usage error below is only + for the "no argument at all" case, not "name not found", and (same "0 + success, 1 failure" shape listDir/catFile/makeDir/changeDir/doExport + above use) the only way this can ever return failure. */ +static int doUnset(char* argument) +{ + if (argument[0] == 0) { + kprintf("unset: usage: unset NAME\n"); + return 1; + } + currentThreadUnsetEnv(argument); + return 0; +} + +/* `env` -- lists every "NAME=VALUE" this shell thread currently holds + (currentThreadEnvCount/currentThreadEnvEntry, sched.h), the same raw + storage format doExport above writes and shellExpandArgv's $NAME lookups + read. */ +static void printEnv(void) +{ + int count = currentThreadEnvCount(); + for (int i = 0; i < count; i++) + kprintf("env: %s\n", currentThreadEnvEntry(i)); } static void doEcho(char* argument) @@ -1357,7 +1418,131 @@ static void doEcho(char* argument) } } -static void execute(char* line) +/* Upper bound on how many `$(...)` command substitutions execute() will + expand on one typed line -- the same "generous cap for one + lineCapacity-bounded line, not a claim real shells have such a limit" + reasoning shellMaxPipelineStages/shellMaxAndOrSegments already give + their own per-line caps. */ +#define shellMaxCommandSubst 4 + +/* Expands every non-nested `$(cmd [args])` in `srcLine` into `outLine`, + left to right -- called from executeSingle below only once `&&`/`||` + (execute()'s own shellSplitAndOr, over the ORIGINAL typed line) and `|` + (executeSingle's own shellSplitPipelineStages, over THIS segment) have + already been resolved, so a literal `&&`/`||`/`|` byte sitting inside a + substituted command's own captured output can never be mistaken for a + real operator -- those are only ever recognized once, before any + expansion runs, exactly like a real shell's own grammar (operators are + parsed structurally first; expansion happens per-word afterward, never + the other way around). + Builds the result into a PRIVATE `work` copy first, splicing in place + there, rather than mutating `srcLine` itself: `srcLine` here is not + necessarily its own standalone buffer -- shellSplitAndOr/ + shellSplitPipelineStages both split their input IN PLACE by writing NUL + terminators into ONE shared backing array (shell.h's own doc comments on + both), so a segment that still has ANOTHER segment's text sitting + immediately after its own NUL terminator would have that neighbor + silently corrupted by any in-place splice that grows past the original + text's own length. Working in a fully separate `work`/`outLine` buffer + makes that impossible regardless of how much the line grows or shrinks. + Each `$(...)` found (shellFindCommandSubst, shell.h) is run to + completion via jobctlCaptureOutput -- a REAL spawn, not a builtin (no fd + of its own to capture from, the same "no stage may be a builtin" scope + runPipelineStages already documents) -- with its own argv given + `$NAME`/`$?` expansion (shellExpandArgv) but nothing else: no wildcard + globs, no redirection, no pipes, no further `$(...)` nesting. Trailing + newlines are stripped from the captured output, matching a real shell's + own `$(...)`. Only ever scans PAST whatever it just spliced into `work` + (`scanFrom` below), never re-scanning a command's own captured output + for a further `$(` -- the same single-pass, no-second-expansion rule a + real shell gives command substitution's own result. + Returns true having written the fully expanded line into `outLine` + (unchanged from `srcLine` if there was nothing to expand), or false, + having already reported the error via kprintf and left `outLine` + untouched, on a malformed `$(` with no matching `)`, an unknown command, + a spliced result that would overflow lineCapacity, or more than + shellMaxCommandSubst substitutions on one line. */ +static bool shellExpandCommandSubst(const char* srcLine, char* outLine, int outCap) +{ + char work[lineCapacity]; + snprintf(work, sizeof(work), "%s", srcLine); + + int scanFrom = 0; + + for (int iterations = 0; iterations < shellMaxCommandSubst; iterations++) { + int start, end; + char cmd[lineCapacity]; + if (!shellFindCommandSubst(work + scanFrom, &start, &end, cmd, sizeof(cmd))) { + kprintf("shell: '$(' with no matching ')'\n"); + return false; + } + if (start < 0) { + snprintf(outLine, (uint64)outCap, "%s", work); + return true; + } + start += scanFrom; + end += scanFrom; + + char* word; + char* rest; + splitCommandWord(cmd, &word, &rest); + + char* argv[shellMaxArgs]; + int argc = 0; + argv[argc++] = word; + argc += shellTokenize(rest, &argv[argc], shellMaxArgs - argc); + + char expandedArgv[shellMaxArgs][envEntryMax]; + shellExpandArgv(argv + 1, argc - 1, expandedArgv + 1); + + char binPath[binPathCapacity]; + snprintf(binPath, sizeof(binPath), "/bin/%s", word); + + char captured[lineCapacity]; + int64 status; + bool spawned = jobctlCaptureOutput(binPath, (const char* const*)argv, argc, + captured, sizeof(captured), &status); + /* Deliberately discarded: real shells leave `$?` reflecting the + OUTER command that actually runs after substitution completes, + never the substituted command itself (executeSingle/execute + already set lastStatus from whatever this line goes on to + spawn). */ + (void)status; + if (!spawned) { + kprintf("%s: command substitution failed (unknown command)\n", word); + return false; + } + + int capturedLen = (int)strlen(captured); + while (capturedLen > 0 && captured[capturedLen - 1] == '\n') + captured[--capturedLen] = 0; + + int tailLen = (int)strlen(work + end); + if (start + capturedLen + tailLen + 1 > lineCapacity) { + kprintf("shell: command substitution result too long\n"); + return false; + } + + memmove(work + start + capturedLen, work + end, (uint64)(tailLen + 1)); + memcpy(work + start, captured, (uint64)capturedLen); + + scanFrom = start + capturedLen; + } + + kprintf("shell: too many '$(' substitutions (max %d)\n", shellMaxCommandSubst); + return false; +} + +/* Runs exactly one command-or-pipeline (never itself another `&&`/`||` + chain -- shellSplitAndOr has already peeled those apart before this is + ever called, see execute() below) and returns its real exit status: a + builtin's own 0/1 (listDir/catFile/makeDir/changeDir/doExport/doUnset), + 0 for every other builtin (none of them has a documented failure case), + or whatever runProgram/runPipelineStages themselves return for a + spawned command or pipeline. This used to be the entire body of + execute() itself, before shell.c had any notion of chaining multiple + commands' exit statuses together. */ +static int64 executeSingle(char* line) { if (line[0] == 0) return;