From 28d1248711c60e1063f8c158e3fb27ddbac9e938 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 21:50:04 +0200 Subject: [PATCH] feat: extend shell with runProgram and splitCommandWord --- src/shell.c | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/shell.c b/src/shell.c index 1af9b94..eb7cadf 100644 --- a/src/shell.c +++ b/src/shell.c @@ -538,6 +538,179 @@ static int shellExpandGlobs(char** argv, int argc, int maxArgc, char storage[][s result[resultCount] = storage[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) {