diff --git a/src/shell.c b/src/shell.c index 2ea3e38..71a84f1 100644 --- a/src/shell.c +++ b/src/shell.c @@ -192,6 +192,179 @@ int shellSplitAndOr(char* line, char* segments[], ShellAndOrOp andOrOps[], int m int opCount = 0; for (const char* cursor = line; *cursor; ) { if ((cursor[0] == '&' && cursor[1] == '&') || (cursor[0] == '|' && cursor[1] == '|')) { + opCount++; + cursor += 2; + } else { + cursor++; + } + } + + if (opCount == 0) + return 0; + if (opCount + 1 > maxSegments) + return -1; + + int count = 0; + char* segStart = line; + char* cursor = line; + while (true) { + bool atAnd = (cursor[0] == '&' && cursor[1] == '&'); + bool atOr = (cursor[0] == '|' && cursor[1] == '|'); + bool atEnd = (*cursor == 0); + if (atAnd || atOr || atEnd) { + char* segEnd = cursor; + while (segEnd > segStart && segEnd[-1] == ' ') + segEnd--; + *segEnd = 0; + + segments[count++] = segStart; + + if (atEnd) + break; + + andOrOps[count - 1] = atAnd ? shellAndOrAnd : shellAndOrOr; + + cursor += 2; + segStart = cursor; + while (*segStart == ' ') + segStart++; + cursor = segStart; + continue; + } + cursor++; + } + + return count; +} + +bool shellExtractRedirects(char** argv, int* argc, ShellRedirect* out) +{ + int count = *argc; + + /* Validate BEFORE mutating anything: a dangling operator (no filename + token after it) must leave argv and argc completely untouched, + exactly like shellSplitPipelineStages leaves `line` untouched when it + finds no pipe -- the caller needs to tell "malformed, nothing + changed" apart from "changed, here is the result" without having to + snapshot argv itself first. */ + for (int i = 0; i < count; i++) { + bool isOperator = strcmp(argv[i], "<") == 0 || strcmp(argv[i], "<<<") == 0 || + strcmp(argv[i], ">") == 0 || strcmp(argv[i], ">>") == 0 || + strcmp(argv[i], "2>") == 0 || strcmp(argv[i], "2>>") == 0; + if (isOperator && i + 1 >= count) + return false; + } + + out->inPath = nullptr; + out->outPath = nullptr; + out->appendOut = false; + out->errPath = nullptr; + out->appendErr = false; + out->hereString = nullptr; + + int writeIndex = 0; + for (int i = 0; i < count; i++) { + if (strcmp(argv[i], "<") == 0) { + out->inPath = argv[++i]; + out->hereString = nullptr; + } else if (strcmp(argv[i], "<<<") == 0) { + out->hereString = argv[++i]; + out->inPath = nullptr; + } else if (strcmp(argv[i], ">") == 0) { + out->outPath = argv[++i]; + out->appendOut = false; + } else if (strcmp(argv[i], ">>") == 0) { + out->outPath = argv[++i]; + out->appendOut = true; + } else if (strcmp(argv[i], "2>") == 0) { + out->errPath = argv[++i]; + out->appendErr = false; + } else if (strcmp(argv[i], "2>>") == 0) { + out->errPath = argv[++i]; + out->appendErr = true; + } else { + argv[writeIndex++] = argv[i]; + } + } + *argc = writeIndex; + return true; +} + +bool shellParseVarRef(const char* token, char* nameOut, int nameCap) +{ + if (token[0] != '$' || token[1] == 0) + return false; + + int len = 0; + for (const char* c = token + 1; *c; c++) { + bool isNameChar = (*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || + (*c >= '0' && *c <= '9') || *c == '_'; + if (!isNameChar) + return false; + len++; + } + if (len + 1 > nameCap) + return false; + + memcpy(nameOut, token + 1, (uint64)len); + nameOut[len] = 0; + return true; +} + +bool shellIsStatusVarToken(const char* token) +{ + return strcmp(token, "$?") == 0; +} + +/* Shared expansion rule behind both shellExpandArgv (per-argv-position, an + entire spawned program's argument list) and shellExpandRedirect + (per-redirect-target) below: `$?` (shellIsStatusVarToken, this shell's + own most-recently-completed status, lastStatus above) is checked FIRST + since shellParseVarRef can never match it anyway (see + shellIsStatusVarToken's own doc comment in shell.h), then a real + `$NAME` environment lookup (shellParseVarRef + currentThreadGetEnv, an + unset name expanding to an empty string like a real shell's own + default). Anything that is neither is returned completely untouched, + still pointing into whatever buffer the caller originally handed it -- + `storage` is only ever written to, and only ever the return value, when + an expansion actually happened. */ +static const char* shellExpandToken(const char* token, char* storage, int storageCap) +{ + if (shellIsStatusVarToken(token)) { + snprintf(storage, (uint64)storageCap, "%ld", lastStatus); + return storage; + } + + char name[envEntryMax]; + if (shellParseVarRef(token, name, sizeof(name))) { + const char* value = currentThreadGetEnv(name); + snprintf(storage, (uint64)storageCap, "%s", value ? value : ""); + return storage; + } + + return token; +} + +/* Resolves every `$?`/`$NAME` token in argv[0..argc) via shellExpandToken + above, overwriting argv[i] with a pointer into storage[i] whenever one + expands -- necessary because argv[i] itself is a pointer into the + shared line buffer with only enough room for the ORIGINAL (unexpanded) + token, never enough for whatever length the variable's value happens to + be. */ +static void shellExpandArgv(char** argv, int argc, char storage[][envEntryMax]) +{ + for (int i = 0; i < argc; i++) + argv[i] = (char*)shellExpandToken(argv[i], storage[i], envEntryMax); +} + +/* Expands `$?`/`$NAME` (shellExpandToken above) in each of ShellRedirect's + own target fields (inPath/outPath/errPath/hereString) in place -- these + come straight from shellExtractRedirects, which only ever pulls raw + tokens out of argv with no expansion of its own, so a redirect target or + here-string text written as `$NAME`/`$?` was previously left completely + unexpanded (unlike every other argv position, which shellExpandArgv + already covered) -- an honestly-scoped gap flagged when `$NAME` + expansion was first added, closed here. Each field gets its own static void printHelp(void) {