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);
|
kprintf("cd: no such directory: %s\n", path);
|
||||||
return 1;
|
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)
|
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)
|
if (line[0] == 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in new issue
Block a user