feat: extend shell with shellExtractRedirects and shellParseVarRef

This commit is contained in:
dualfroz committed 2026-09-07 19:35:10 +02:00
1 parent 96148ff0e7
commit 6bc12b43a7
1 file changed
+173
+173
View File
@@ -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)
{