diff --git a/src/shell.c b/src/shell.c index 71a84f1..1af9b94 100644 --- a/src/shell.c +++ b/src/shell.c @@ -365,6 +365,179 @@ static void shellExpandArgv(char** argv, int argc, char storage[][envEntryMax]) 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 + dedicated storage slot since all four could need to be expanded on the + SAME command line at once (e.g. `cmd < $IN > $OUT 2> $ERR`). */ +static void shellExpandRedirect(ShellRedirect* redirect, char storage[4][envEntryMax]) +{ + if (redirect->inPath) + redirect->inPath = shellExpandToken(redirect->inPath, storage[0], envEntryMax); + if (redirect->outPath) + redirect->outPath = shellExpandToken(redirect->outPath, storage[1], envEntryMax); + if (redirect->errPath) + redirect->errPath = shellExpandToken(redirect->errPath, storage[2], envEntryMax); + if (redirect->hereString) + redirect->hereString = shellExpandToken(redirect->hereString, storage[3], envEntryMax); +} + +bool shellFindCommandSubst(const char* line, int* startOut, int* endOut, char* cmdOut, int cmdCap) +{ + const char* open = strstr(line, "$("); + if (!open) { + *startOut = -1; + return true; + } + + const char* close = strchr(open + 2, ')'); + if (!close) + return false; + + int len = (int)(close - (open + 2)); + if (len > cmdCap - 1) + len = cmdCap - 1; + memcpy(cmdOut, open + 2, (uint64)len); + cmdOut[len] = 0; + + *startOut = (int)(open - line); + *endOut = (int)(close - line) + 1; + return true; +} + +bool shellGlobMatch(const char* pattern, const char* name) +{ + const char* p = pattern; + const char* n = name; + const char* starPattern = nullptr; + const char* starName = nullptr; + + while (*n) { + if (*p == '?' || *p == *n) { + p++; + n++; + } else if (*p == '*') { + starPattern = p++; + starName = n; + } else if (starPattern) { + p = starPattern + 1; + n = ++starName; + } else { + return false; + } + } + while (*p == '*') + p++; + return *p == 0; +} + +bool shellTokenHasGlob(const char* token) +{ + for (const char* c = token; *c; c++) + if (*c == '*' || *c == '?') + return true; + return false; +} + +void shellSplitGlobToken(const char* token, char* dirOut, int dirCap, char* patternOut, int patternCap) +{ + const char* lastSlash = nullptr; + for (const char* c = token; *c; c++) + if (*c == '/') + lastSlash = c; + + if (!lastSlash) { + if (dirCap > 0) + dirOut[0] = 0; + snprintf(patternOut, (uint64)patternCap, "%s", token); + return; + } + + int dirLen = (int)(lastSlash - token) + 1; + if (dirLen >= dirCap) + dirLen = dirCap - 1; + memcpy(dirOut, token, (uint64)dirLen); + dirOut[dirLen] = 0; + + snprintf(patternOut, (uint64)patternCap, "%s", lastSlash + 1); +} + +/* Storage capacity for one shellExpandGlobs match's resolved path -- a + smaller, dedicated constant rather than reusing pathCapacity, the same + "arbitrary but documented" fixed-size trade-off shellExpandArgv's own + envEntryMax already makes for a $NAME value, since this buffer gets + multiplied by shellMaxArgs (and, in runPipelineStages, by + shellMaxPipelineStages again on top of that) -- see shellExpandGlobs' + own doc comment for why that multiplication matters for a 16KiB kernel + thread stack (sched.c's threadStackSize). */ +#define shellGlobPathCapacity 64 + +/* Expands any argv[i] (i in [0,argc)) containing a glob metacharacter into + the real directory entries matching it (VFS child-list order, vfsChild/ + vfsNext -- the same traversal listDir's own `ls` builtin above already + uses), replacing that one token with however many real paths matched, + written into `storage` (one shellGlobPathCapacity-byte slot per FINAL + argv position, not per original token) and pointed at from `argv` in + place. A token with no glob character is passed through completely + unchanged. A token that DOES contain a glob character but matches NO + real file is ALSO passed through completely unchanged, as its own + literal single token (bash's own default nullglob=off behavior) -- so a + program that tries to open a nonexistent glob gets the same "no such + file" error a real shell gives for an unmatched pattern, not a silently + vanished argument. Only the FINAL path component may contain glob syntax + (shellSplitGlobToken's own doc comment) -- `/tmp/gd/ *.txt` is supported, + `/tmp/ * /file.txt` is not. The directory part is resolved through + vfsResolvePath first, so a bare `*.txt` (no '/' at all) globs the + CALLING thread's own current directory, same cwd-awareness every other + path-taking builtin in this file already gets. Deliberately called by + runProgram/runPipelineStages AFTER shellExtractRedirects has already + pulled out redirection operators and their targets, and after the + trailing "&" background marker has already been stripped -- so a glob + character can never appear in a redirect target or be mistaken for + something other than a real program argument; an honestly-scoped + limitation matching this file's existing "no stage may be a builtin"/ + redirect-target-scope documented choices elsewhere. Returns the new argc, + or -1 (argv left completely untouched) if expansion would need more than + `maxArgc` slots -- the same "malformed/too-large input, caller reports an + error instead of silently truncating" shape shellSplitPipelineStages + already uses for "too many stages". */ +static int shellExpandGlobs(char** argv, int argc, int maxArgc, char storage[][shellGlobPathCapacity]) +{ + char* result[shellMaxArgs]; + int resultCount = 0; + + for (int i = 0; i < argc; i++) { + if (!shellTokenHasGlob(argv[i])) { + if (resultCount >= maxArgc) + return -1; + result[resultCount++] = argv[i]; + continue; + } + + char dirPart[pathCapacity]; + char patternPart[pathCapacity]; + shellSplitGlobToken(argv[i], dirPart, sizeof(dirPart), patternPart, sizeof(patternPart)); + + char resolvedDir[pathCapacity]; + vfsResolvePath(dirPart, resolvedDir, sizeof(resolvedDir)); + + int matchStart = resultCount; + VfsNode* dirNode = vfsLookup(resolvedDir); + if (dirNode && vfsIsDir(dirNode)) { + for (VfsNode* child = vfsChild(dirNode); child; child = vfsNext(child)) { + if (!shellGlobMatch(patternPart, vfsName(child))) + continue; + if (resultCount >= maxArgc) + return -1; + + int pos = snprintf(storage[resultCount], shellGlobPathCapacity, "%s", resolvedDir); + if (pos < 0) + pos = 0; + if (pos >= shellGlobPathCapacity) + pos = shellGlobPathCapacity - 1; + snprintf(storage[resultCount] + pos, (uint64)(shellGlobPathCapacity - pos), "%s", + vfsName(child)); + + result[resultCount] = storage[resultCount]; + resultCount++; + } static void printHelp(void) {