feat: extend shell with shellExpandRedirect and shellFindCommandSubst

This commit is contained in:
dualfroz committed 2026-09-07 20:55:39 +02:00
1 parent 6bc12b43a7
commit ff86a70128
1 file changed
+173
+173
View File
@@ -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)
{