feat: extend shell with doExport and doUnset
This commit is contained in:
1 parent
230e271ea1
commit
ebdc94d031
1 file changed
+187
-2
+187
-2
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user