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 unexpanded (unlike every other argv position, which shellExpandArgv
already covered) -- an honestly-scoped gap flagged when `$NAME` already covered) -- an honestly-scoped gap flagged when `$NAME`
expansion was first added, closed here. Each field gets its own 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) static void printHelp(void)
{ {