feat: extend shell with doExport and doUnset

This commit is contained in:
dualfroz committed 2026-09-08 13:18:03 +02:00
1 parent 230e271ea1
commit ebdc94d031
1 file changed
+187 -2
+187 -2
View File
@@ -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;