feat: extend shell with runProgram and splitCommandWord

This commit is contained in:
dualfroz committed 2026-09-07 21:50:04 +02:00
1 parent ff86a70128
commit 28d1248711
1 file changed
+173
+173
View File
@@ -538,6 +538,179 @@ static int shellExpandGlobs(char** argv, int argc, int maxArgc, char storage[][s
result[resultCount] = storage[resultCount]; result[resultCount] = storage[resultCount];
resultCount++; resultCount++;
} }
}
if (resultCount == matchStart) {
if (resultCount >= maxArgc)
return -1;
result[resultCount++] = argv[i];
}
}
for (int i = 0; i < resultCount; i++)
argv[i] = result[i];
return resultCount;
}
/* Fallback for a command that matched none of the built-ins in execute()
below: `line` is already the first word (NUL-terminated in place by
execute()'s own split) and `argument` is the rest of the original typed
line, still starting right after that NUL in the very same buffer. */
/* Returns the real exit status of whatever this line ran (0/nonzero from
the spawned process itself for a foreground command, shellExecNotFound
above for anything that never even got to spawn, or 0 immediately for a
backgrounded job -- matching real `cmd &`'s own immediate-success exit
status, never the job's own eventual, still-unknown-at-this-point exit
code) -- feeds shell.c's new `&&`/`||` chaining (execute()) the same way
listDir/catFile/makeDir/changeDir/doExport/doUnset's own int returns
already do for builtins. */
static int64 runProgram(char* line, char* argument)
{
char* argv[shellMaxArgs];
int argc = 0;
argv[argc++] = line;
argc += shellTokenize(argument, &argv[argc], shellMaxArgs - argc);
/* argv[0] (== line, the program name) is deliberately left out of
expansion: runProgram resolves the binary to spawn from `line`
itself, never from argv[0], so expanding it would let argv[0] show
the child a different name than the path actually loaded. */
char expandedArgv[shellMaxArgs][envEntryMax];
shellExpandArgv(argv + 1, argc - 1, expandedArgv + 1);
bool background = (argc > 1 && strcmp(argv[argc - 1], "&") == 0);
if (background)
argc--;
/* `< IN`/`<<< TEXT`/`> OUT`/`>> OUT`/`2> ERR`/`2>> ERR` are stripped out
of argv here, AFTER the trailing "&" check above (so "&" itself is
never mistaken for a redirection target) and BEFORE the program ever
sees argv -- exactly like a real shell, which never hands its own
redirection operators to the child as arguments. */
ShellRedirect redirect;
if (!shellExtractRedirects(argv, &argc, &redirect)) {
kprintf("%s: redirection operator with no filename\n", line);
return shellExecNotFound;
}
char redirectStorage[4][envEntryMax];
shellExpandRedirect(&redirect, redirectStorage);
/* Wildcard expansion runs LAST, on the real argv the child program will
actually receive -- after redirects/here-string/background have all
already been pulled out, so a glob character can only ever expand a
genuine program argument (see shellExpandGlobs' own doc comment for
why). argv[0] (the program name) is excluded, same reasoning as
shellExpandArgv's own exclusion just above. */
char globStorage[shellMaxArgs][shellGlobPathCapacity];
int expandedArgc = shellExpandGlobs(argv + 1, argc - 1, shellMaxArgs - 1, globStorage);
if (expandedArgc < 0) {
kprintf("%s: too many arguments after wildcard expansion\n", line);
return shellExecNotFound;
}
argc = 1 + expandedArgc;
char binPath[binPathCapacity];
snprintf(binPath, sizeof(binPath), "/bin/%s", line);
if (background) {
uint32 pid;
/* A here-string needs its own spawn path (jobctlRunBackgroundHereString
wires the child's fd 0 to a real pipe the SHELL itself writes the
text into, rather than opening a path) -- redirect.inPath is
always nullptr whenever hereString is set (shellExtractRedirects'
own last-one-wins rule between the two), so the plain Redirected
call below would otherwise just leave fd 0 at the console. */
bool spawned = redirect.hereString
? jobctlRunBackgroundHereString(binPath, (const char* const*)argv, argc,
redirect.hereString, redirect.outPath,
redirect.appendOut, redirect.errPath,
redirect.appendErr, &pid)
: jobctlRunBackgroundRedirected(binPath, (const char* const*)argv, argc,
redirect.inPath, redirect.outPath, redirect.appendOut,
redirect.errPath, redirect.appendErr, &pid);
if (!spawned) {
kprintf("unknown command or bad redirection: %s (try 'help')\n", line);
return shellExecNotFound;
}
for (int i = 0; i < maxBackgroundJobs; i++) {
if (!backgroundJobs[i].used) {
backgroundJobs[i].used = true;
backgroundJobs[i].pid = pid;
snprintf(backgroundJobs[i].name, sizeof(backgroundJobs[i].name), "%s", line);
break;
}
}
lastSpawnedPid = pid;
kprintf("[%u] %s started in background\n", pid, line);
return 0;
}
/* Split into spawn-then-wait (rather than one combined call) so
lastSpawnedPid below is visible to anyone asking (shellLastSpawnedPid)
the instant the job exists, not only after jobctlWaitForeground's
blocking wait has already finished -- the same immediacy the
background branch above already gets for free from jobctlRunBackground
never blocking in the first place. Same here-string-vs-plain split as
the background branch above, same reason. */
uint32 pid = redirect.hereString
? jobctlSpawnForegroundHereString(binPath, (const char* const*)argv, argc,
redirect.hereString, redirect.outPath,
redirect.appendOut, redirect.errPath, redirect.appendErr)
: jobctlSpawnForegroundRedirected(binPath, (const char* const*)argv, argc,
redirect.inPath, redirect.outPath,
redirect.appendOut, redirect.errPath,
redirect.appendErr);
if (pid == threadInvalidId) {
kprintf("unknown command or bad redirection: %s (try 'help')\n", line);
return shellExecNotFound;
}
lastSpawnedPid = pid;
int64 exitCode = jobctlWaitForeground(pid);
kprintf("%s exited with code %ld\n", line, exitCode);
return exitCode;
}
/* Splits each of an already-`shellSplitPipelineStages`d line's stages into
its own binary name + argv, exactly like runProgram's own split above --
each stage is independently "the first word, then the rest" in the SAME
shape execute() already hands runProgram, just done once per stage here
instead of once total. */
static void splitCommandWord(char* text, char** word, char** rest)
{
char* cursor = text;
while (*cursor && *cursor != ' ')
cursor++;
if (*cursor == ' ') {
*cursor = 0;
cursor++;
}
while (*cursor == ' ')
cursor++;
*word = text;
*rest = cursor;
}
/* Real N-stage shell pipeline (2 <= count <= shellMaxPipelineStages):
stage i's stdout feeds stage i+1's stdin through a real pipe
(vfsPipeCreateHandle + jobctlSpawnPiped's fd-baking, jobctl.h) for every
adjacent pair -- stage 0 otherwise keeps the real console for its stdin,
the last stage otherwise keeps it for its stdout, exactly like a real
shell's own `cmd1 | cmd2 | ... | cmdN`. jobctlSpawnPiped already takes
independent stdin/stdout pipe handles per call, so a MIDDLE stage (both
ends piped at once) needs no new primitive here, only the loop below
wiring each stage's stdin to the previous pipe and its stdout to the
next one. No stage may itself be a builtin (echo/ls/cat/... run directly
on THIS thread via kprintf/vfs, never through a spawned process's own fd
1, so they have no real fd to redirect) -- an honestly-scoped limitation
matching jobctl.h's own doc comments elsewhere in this file. Only stage
0's stdin may additionally be a here-string (`cmd1 <<< TEXT | cmd2 | ...`,
jobctlSpawnPipedHereString below) -- every later stage's stdin is already
a pipe, exactly like the old two-stage runPipeline already required.
Returns the real exit status of the whole pipeline (the LAST stage's own
exit code, matching real `$?`, or shellExecNotFound for any error that
never even got every stage spawned) -- same "feeds shell.c's new
`&&`/`||` chaining" reasoning as runProgram's own return above. */
static int64 runPipelineStages(char* stages[], int count)
static void printHelp(void) static void printHelp(void)
{ {