Files
frostbyte/src/shell.c
T

1734 lines
67 KiB
C

#include "shell.h"
#include "console.h"
#include "keyboard.h"
#include "ports.h"
#include "vga.h"
#include "pmm.h"
#include "heap.h"
#include "pit.h"
#include "sched.h"
#include "string.h"
#include "vfs.h"
#include "fs.h"
#include "jobctl.h"
#include "klib.h"
#include "pipe.h"
#include "rtc.h"
#define lineCapacity 128
#define pathCapacity 256
#define binPathCapacity 272
/* Real shell "command could not even be started" exit status (bad
redirection, wildcard overflow, unknown binary, ...) -- the same 127
real shells report for "command not found", reused here for every
pre-spawn failure in runProgram/runPipelineStages so shell.c's new
`&&`/`||` chaining (execute()) always sees a nonzero, "this failed"
status for them, exactly like a real spawned process's own nonzero
exit code would. */
#define shellExecNotFound 127
/* The shell used to own the only "current directory" concept in this whole
kernel, a single static local shared by nothing else -- sched.h's new
Thread.cwd (per-thread, real chdir(2)/getcwd(2) semantics, vfs.h's
vfsChdir/vfsGetcwd/vfsResolvePath) replaces it: the shell is a plain
kernel thread like any other (see shellRun's own comment below), so it
simply becomes one more caller of the same real subsystem instead of
keeping a second, disconnected notion of "cwd" in parallel. This also
means a job this shell spawns now genuinely inherits whatever directory
the shell itself is currently in (initialCwd, sched.c) -- true before
this fala only by accident of both defaulting to "/", never actually
wired together. */
/*
* Real job control for the interactive shell (jobctl.h does the actual
* spawning/terminal-ownership work): any command line that is not one of
* the built-ins above is tried as /bin/<word> instead of just being
* rejected outright, with the rest of the line tokenized into a real argv
* exactly like a normal shell does. A trailing lone "&" token backgrounds
* it (jobctlRunBackground, no terminal, returns immediately) instead of
* running it in the foreground (jobctlSpawnForeground + jobctlWaitForeground,
* which blocks until it exits with the terminal genuinely handed to it --
* see jobctl.c).
*/
#define maxBackgroundJobs 8
typedef struct BackgroundJob {
uint32 pid;
char name[32];
bool used;
} BackgroundJob;
static BackgroundJob backgroundJobs[maxBackgroundJobs];
static uint32 lastSpawnedPid = threadInvalidId;
/* Real shell `$?` -- the exit status of the most recently completed
command-or-pipeline-or-builtin, persisting across SEPARATELY typed
lines (unlike the `&&`/`||` chaining `status` local in execute() below,
which only ever lives for the duration of one typed line's own
segments). Initialized to 0, matching a real shell's own `$?` before
anything has ever run. Updated in exactly one place (execute(), right
after every executeSingle() call, chained or not) and read in exactly
one place (shellExpandToken, via shellIsStatusVarToken) -- the same
"shell is a plain kernel thread, needs no syscall to reach its own
state" shape doExport/doUnset/changeDir already use for their own
thread-local state, just kept in a file-scope static here since $? is
a shell-language special parameter, not a real environment variable
(real shells never let you `export $?` either). */
static int64 lastStatus = 0;
uint32 shellLastSpawnedPid(void)
{
return lastSpawnedPid;
}
/* Opportunistically reaps any of THIS shell's own children that have
already exited (threadJoinAnyChild, sched.h -- the same non-blocking
waitpid(-1)-shaped scan user/waitanytest.c already proves) and prints
what finished. Only ever finds background jobs here: a foreground job is
already fully joined-and-reaped by jobctlWaitForeground itself before
that call ever returns, so by the time control comes back to this
function there is nothing left over from it to double-reap. */
static void reapFinishedBackgroundJobs(void)
{
uint32 deadId;
int64 code;
while (threadJoinAnyChild(currentThreadId(), &deadId, &code) == waitScanFoundDead) {
for (int i = 0; i < maxBackgroundJobs; i++) {
if (backgroundJobs[i].used && backgroundJobs[i].pid == deadId) {
kprintf("[%u] %s done, exit code %ld\n", deadId, backgroundJobs[i].name, code);
backgroundJobs[i].used = false;
break;
}
}
}
}
static void listBackgroundJobs(void)
{
bool any = false;
for (int i = 0; i < maxBackgroundJobs; i++) {
if (backgroundJobs[i].used) {
kprintf("[%u] running %s\n", backgroundJobs[i].pid, backgroundJobs[i].name);
any = true;
}
}
if (!any)
kprintf("no background jobs\n");
}
int shellTokenize(char* text, char* tokens[], int maxTokens)
{
int count = 0;
char* cursor = text;
while (*cursor && count < maxTokens) {
while (*cursor == ' ')
cursor++;
if (*cursor == 0)
break;
tokens[count++] = cursor;
while (*cursor && *cursor != ' ')
cursor++;
if (*cursor == ' ') {
*cursor = 0;
cursor++;
}
}
return count;
}
int shellSplitPipelineStages(char* line, char* stages[], int maxStages)
{
/* First pass: count top-level '|' occurrences without mutating
anything, so a too-many-stages line can be rejected with `line`
completely untouched -- the same "validate before mutating" shape
shellExtractRedirects already uses for its own dangling-operator
check. */
int pipeCount = 0;
for (const char* cursor = line; *cursor; cursor++)
if (*cursor == '|')
pipeCount++;
if (pipeCount == 0)
return 0;
if (pipeCount + 1 > maxStages)
return -1;
int count = 0;
char* stageStart = line;
char* cursor = line;
while (true) {
bool atPipe = (*cursor == '|');
bool atEnd = (*cursor == 0);
if (atPipe || atEnd) {
char* stageEnd = cursor;
while (stageEnd > stageStart && stageEnd[-1] == ' ')
stageEnd--;
*stageEnd = 0;
stages[count++] = stageStart;
if (atEnd)
break;
*cursor = 0;
stageStart = cursor + 1;
while (*stageStart == ' ')
stageStart++;
}
cursor++;
}
return count;
}
int shellSplitAndOr(char* line, char* segments[], ShellAndOrOp andOrOps[], int maxSegments)
{
/* Same "count first, mutate nothing, reject up front" shape
shellSplitPipelineStages uses for its own analogous limit --
matching two-character operators one at a time (never overlapping:
stepping by 2 on a match means "&&&" is one "&&" followed by a lone
'&' left inside a segment, not two overlapping matches). */
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
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++;
}
}
if (resultCount == matchStart) {
if (resultCount >= maxArgc)
return -1;
result[resultCount++] = argv[i];
}
}
for (int i = 0; i < resultCount; i++)
argv[i] = result[i];
return resultCount;
}
/* Fallback for a command that matched none of the built-ins in execute()
below: `line` is already the first word (NUL-terminated in place by
execute()'s own split) and `argument` is the rest of the original typed
line, still starting right after that NUL in the very same buffer. */
/* Returns the real exit status of whatever this line ran (0/nonzero from
the spawned process itself for a foreground command, shellExecNotFound
above for anything that never even got to spawn, or 0 immediately for a
backgrounded job -- matching real `cmd &`'s own immediate-success exit
status, never the job's own eventual, still-unknown-at-this-point exit
code) -- feeds shell.c's new `&&`/`||` chaining (execute()) the same way
listDir/catFile/makeDir/changeDir/doExport/doUnset's own int returns
already do for builtins. */
static int64 runProgram(char* line, char* argument)
{
char* argv[shellMaxArgs];
int argc = 0;
argv[argc++] = line;
argc += shellTokenize(argument, &argv[argc], shellMaxArgs - argc);
/* argv[0] (== line, the program name) is deliberately left out of
expansion: runProgram resolves the binary to spawn from `line`
itself, never from argv[0], so expanding it would let argv[0] show
the child a different name than the path actually loaded. */
char expandedArgv[shellMaxArgs][envEntryMax];
shellExpandArgv(argv + 1, argc - 1, expandedArgv + 1);
bool background = (argc > 1 && strcmp(argv[argc - 1], "&") == 0);
if (background)
argc--;
/* `< IN`/`<<< TEXT`/`> OUT`/`>> OUT`/`2> ERR`/`2>> ERR` are stripped out
of argv here, AFTER the trailing "&" check above (so "&" itself is
never mistaken for a redirection target) and BEFORE the program ever
sees argv -- exactly like a real shell, which never hands its own
redirection operators to the child as arguments. */
ShellRedirect redirect;
if (!shellExtractRedirects(argv, &argc, &redirect)) {
kprintf("%s: redirection operator with no filename\n", line);
return shellExecNotFound;
}
char redirectStorage[4][envEntryMax];
shellExpandRedirect(&redirect, redirectStorage);
/* Wildcard expansion runs LAST, on the real argv the child program will
actually receive -- after redirects/here-string/background have all
already been pulled out, so a glob character can only ever expand a
genuine program argument (see shellExpandGlobs' own doc comment for
why). argv[0] (the program name) is excluded, same reasoning as
shellExpandArgv's own exclusion just above. */
char globStorage[shellMaxArgs][shellGlobPathCapacity];
int expandedArgc = shellExpandGlobs(argv + 1, argc - 1, shellMaxArgs - 1, globStorage);
if (expandedArgc < 0) {
kprintf("%s: too many arguments after wildcard expansion\n", line);
return shellExecNotFound;
}
argc = 1 + expandedArgc;
char binPath[binPathCapacity];
snprintf(binPath, sizeof(binPath), "/bin/%s", line);
if (background) {
uint32 pid;
/* A here-string needs its own spawn path (jobctlRunBackgroundHereString
wires the child's fd 0 to a real pipe the SHELL itself writes the
text into, rather than opening a path) -- redirect.inPath is
always nullptr whenever hereString is set (shellExtractRedirects'
own last-one-wins rule between the two), so the plain Redirected
call below would otherwise just leave fd 0 at the console. */
bool spawned = redirect.hereString
? jobctlRunBackgroundHereString(binPath, (const char* const*)argv, argc,
redirect.hereString, redirect.outPath,
redirect.appendOut, redirect.errPath,
redirect.appendErr, &pid)
: jobctlRunBackgroundRedirected(binPath, (const char* const*)argv, argc,
redirect.inPath, redirect.outPath, redirect.appendOut,
redirect.errPath, redirect.appendErr, &pid);
if (!spawned) {
kprintf("unknown command or bad redirection: %s (try 'help')\n", line);
return shellExecNotFound;
}
for (int i = 0; i < maxBackgroundJobs; i++) {
if (!backgroundJobs[i].used) {
backgroundJobs[i].used = true;
backgroundJobs[i].pid = pid;
snprintf(backgroundJobs[i].name, sizeof(backgroundJobs[i].name), "%s", line);
break;
}
}
lastSpawnedPid = pid;
kprintf("[%u] %s started in background\n", pid, line);
return 0;
}
/* Split into spawn-then-wait (rather than one combined call) so
lastSpawnedPid below is visible to anyone asking (shellLastSpawnedPid)
the instant the job exists, not only after jobctlWaitForeground's
blocking wait has already finished -- the same immediacy the
background branch above already gets for free from jobctlRunBackground
never blocking in the first place. Same here-string-vs-plain split as
the background branch above, same reason. */
uint32 pid = redirect.hereString
? jobctlSpawnForegroundHereString(binPath, (const char* const*)argv, argc,
redirect.hereString, redirect.outPath,
redirect.appendOut, redirect.errPath, redirect.appendErr)
: jobctlSpawnForegroundRedirected(binPath, (const char* const*)argv, argc,
redirect.inPath, redirect.outPath,
redirect.appendOut, redirect.errPath,
redirect.appendErr);
if (pid == threadInvalidId) {
kprintf("unknown command or bad redirection: %s (try 'help')\n", line);
return shellExecNotFound;
}
lastSpawnedPid = pid;
int64 exitCode = jobctlWaitForeground(pid);
kprintf("%s exited with code %ld\n", line, exitCode);
return exitCode;
}
/* Splits each of an already-`shellSplitPipelineStages`d line's stages into
its own binary name + argv, exactly like runProgram's own split above --
each stage is independently "the first word, then the rest" in the SAME
shape execute() already hands runProgram, just done once per stage here
instead of once total. */
static void splitCommandWord(char* text, char** word, char** rest)
{
char* cursor = text;
while (*cursor && *cursor != ' ')
cursor++;
if (*cursor == ' ') {
*cursor = 0;
cursor++;
}
while (*cursor == ' ')
cursor++;
*word = text;
*rest = cursor;
}
/* Real N-stage shell pipeline (2 <= count <= shellMaxPipelineStages):
stage i's stdout feeds stage i+1's stdin through a real pipe
(vfsPipeCreateHandle + jobctlSpawnPiped's fd-baking, jobctl.h) for every
adjacent pair -- stage 0 otherwise keeps the real console for its stdin,
the last stage otherwise keeps it for its stdout, exactly like a real
shell's own `cmd1 | cmd2 | ... | cmdN`. jobctlSpawnPiped already takes
independent stdin/stdout pipe handles per call, so a MIDDLE stage (both
ends piped at once) needs no new primitive here, only the loop below
wiring each stage's stdin to the previous pipe and its stdout to the
next one. No stage may itself be a builtin (echo/ls/cat/... run directly
on THIS thread via kprintf/vfs, never through a spawned process's own fd
1, so they have no real fd to redirect) -- an honestly-scoped limitation
matching jobctl.h's own doc comments elsewhere in this file. Only stage
0's stdin may additionally be a here-string (`cmd1 <<< TEXT | cmd2 | ...`,
jobctlSpawnPipedHereString below) -- every later stage's stdin is already
a pipe, exactly like the old two-stage runPipeline already required.
Returns the real exit status of the whole pipeline (the LAST stage's own
exit code, matching real `$?`, or shellExecNotFound for any error that
never even got every stage spawned) -- same "feeds shell.c's new
`&&`/`||` chaining" reasoning as runProgram's own return above. */
static int64 runPipelineStages(char* stages[], int count)
{
char* word[shellMaxPipelineStages];
char* argv[shellMaxPipelineStages][shellMaxArgs];
int argc[shellMaxPipelineStages];
ShellRedirect redirect[shellMaxPipelineStages];
/* Per-stage expansion scratch for shellExpandArgv, same "argv[0] is
never expanded" reasoning as runProgram's own expandedArgv -- each
stage's actual binary comes from word[i], never from argv[i][0]. */
char expanded[shellMaxPipelineStages][shellMaxArgs][envEntryMax];
/* Per-stage wildcard expansion scratch (shellExpandGlobs) -- a
dedicated, smaller shellGlobPathCapacity buffer rather than reusing
pathCapacity, since this array is already multiplied by
shellMaxPipelineStages on top of shellMaxArgs and shares a single
16KiB kernel-thread stack (sched.c's threadStackSize) with every
other local array in this function -- see shellExpandGlobs' own doc
comment in shell.c for the full sizing reasoning. */
char globStorage[shellMaxPipelineStages][shellMaxArgs][shellGlobPathCapacity];
/* Per-stage redirect-target expansion scratch (shellExpandRedirect) --
same reasoning as runProgram's own redirectStorage, one dedicated
4-slot array per stage since a stage's own inPath/outPath/errPath/
hereString could all need expanding on the same line at once. */
char redirectStorage[shellMaxPipelineStages][4][envEntryMax];
for (int i = 0; i < count; i++) {
char* rest;
splitCommandWord(stages[i], &word[i], &rest);
argc[i] = 0;
argv[i][argc[i]++] = word[i];
argc[i] += shellTokenize(rest, &argv[i][argc[i]], shellMaxArgs - argc[i]);
shellExpandArgv(&argv[i][1], argc[i] - 1, &expanded[i][1]);
if (!shellExtractRedirects(argv[i], &argc[i], &redirect[i])) {
kprintf("%s: redirection operator with no filename\n", word[i]);
return shellExecNotFound;
}
shellExpandRedirect(&redirect[i], redirectStorage[i]);
/* Same "expand real arguments last, after redirects" ordering as
runProgram's own single-command path above. */
int expandedArgc = shellExpandGlobs(&argv[i][1], argc[i] - 1, shellMaxArgs - 1, globStorage[i]);
if (expandedArgc < 0) {
kprintf("%s: too many arguments after wildcard expansion\n", word[i]);
return shellExecNotFound;
}
argc[i] = 1 + expandedArgc;
}
/* Only the two OPEN ends of the pipeline may be file/here-string
redirected for stdin/stdout -- stage 0's stdin and the LAST stage's
stdout -- exactly like this function's own header comment already
scopes "no stage may be a builtin" for a similar reason. Every
interior fd is already wired to a real pipe on both sides, so those
combinations are rejected outright below rather than silently
guessing which of "the pipe" or "the file" should win. fd 2
(stderr) is never part of any pipe, so `2>`/`2>>` are always allowed
on every stage independently, with no such restriction. */
for (int i = 0; i < count; i++) {
if (i > 0 && redirect[i].inPath) {
kprintf("pipeline: '%s' already reads from the pipe, input redirection is only "
"supported on the first stage\n", word[i]);
return shellExecNotFound;
}
if (i > 0 && redirect[i].hereString) {
kprintf("pipeline: '%s' already reads from the pipe, here-string redirection is "
"only supported on the first stage\n", word[i]);
return shellExecNotFound;
}
if (i < count - 1 && redirect[i].outPath) {
kprintf("pipeline: '%s' already writes to the pipe, output redirection is only "
"supported on the last stage\n", word[i]);
return shellExecNotFound;
}
}
uint32 pids[shellMaxPipelineStages];
/* pipeHandle[i] connects stage i's stdout to stage i+1's stdin -- only
count-1 links exist for count stages. */
int pipeHandle[shellMaxPipelineStages - 1];
for (int i = 0; i < count; i++) {
int stdinHandle = (i == 0) ? -1 : pipeHandle[i - 1];
int stdoutHandle = -1;
if (i < count - 1) {
pipeHandle[i] = vfsPipeCreateHandle();
if (pipeHandle[i] < 0) {
kprintf("pipeline: no pipe slots free\n");
for (int k = 0; k < i; k++) {
threadKill(pids[k]);
threadJoin(pids[k]);
}
return shellExecNotFound;
}
stdoutHandle = pipeHandle[i];
}
/* joinPgid: every stage after the first joins stage 0's own group
(jobctlSpawnPiped's own doc comment) so a single Ctrl-C, or this
function's own eventual keyboardSetForegroundPgid via
jobctlWaitPipelineStages, reaches every stage at once. */
uint32 joinPgid = (i == 0) ? threadInvalidId : pids[0];
char binPath[binPathCapacity];
snprintf(binPath, sizeof(binPath), "/bin/%s", word[i]);
/* A here-string is only ever possible on stage 0 (validated above)
-- jobctlSpawnPipedHereString wires fd 0 to a real pipe the
SHELL itself writes the text into, rather than leaving it at the
console, then passes stdoutHandle/joinPgid straight through
exactly like the plain jobctlSpawnPiped every other stage
uses. */
uint32 pid = (i == 0 && redirect[i].hereString)
? jobctlSpawnPipedHereString(binPath, (const char* const*)argv[i], argc[i],
redirect[i].hereString, stdoutHandle, joinPgid)
: jobctlSpawnPiped(binPath, (const char* const*)argv[i], argc[i],
stdinHandle, stdoutHandle, joinPgid);
if (pid == threadInvalidId) {
kprintf("unknown command: %s (try 'help')\n", word[i]);
if (i < count - 1)
pipeDestroyIfUnattached(pipeHandle[i]);
for (int k = 0; k < i; k++) {
threadKill(pids[k]);
threadJoin(pids[k]);
}
return shellExecNotFound;
}
pids[i] = pid;
const char* inPath = (i == 0) ? redirect[i].inPath : nullptr;
const char* outPath = (i == count - 1) ? redirect[i].outPath : nullptr;
bool appendOut = (i == count - 1) ? redirect[i].appendOut : false;
if ((inPath || outPath || redirect[i].errPath) &&
!jobctlApplyRedirects(pid, inPath, outPath, appendOut,
redirect[i].errPath, redirect[i].appendErr)) {
kprintf("%s: cannot open %s\n", word[i],
inPath ? inPath : (outPath ? outPath : redirect[i].errPath));
for (int k = 0; k <= i; k++) {
threadKill(pids[k]);
threadJoin(pids[k]);
}
return shellExecNotFound;
}
}
lastSpawnedPid = pids[count - 1];
int64 exitCode = jobctlWaitPipelineStages(pids, count);
char summary[lineCapacity];
int pos = 0;
for (int i = 0; i < count; i++) {
if (i > 0)
pos += snprintf(summary + pos, sizeof(summary) - pos, " | ");
pos += snprintf(summary + pos, sizeof(summary) - pos, "%s", word[i]);
}
kprintf("%s exited with code %ld\n", summary, exitCode);
return exitCode;
}
static void printHelp(void)
{
kprintf("available commands:\n");
kprintf(" help show this list\n");
kprintf(" echo TEXT print the given text\n");
kprintf(" echo TEXT >F write the text to file F\n");
kprintf(" ls [-l] [PATH] list a directory (tmpfs or /mnt), -l adds type/size/mtime\n");
kprintf(" cat FILE print a file\n");
kprintf(" mkdir PATH create a directory (writable mounts only)\n");
kprintf(" rm PATH remove a file (refuses a directory)\n");
kprintf(" rmdir PATH remove an empty directory\n");
kprintf(" chmod MODE PATH set owner/group/other rwx bits (octal, e.g. 644 or 750)\n");
kprintf(" chgrp GID PATH set group (decimal); owner may only set its own gid\n");
kprintf(" cd PATH change the working directory\n");
kprintf(" pwd print the working directory\n");
kprintf(" whoami print this shell's own uid (0 = root)\n");
kprintf(" mem physical memory and heap usage\n");
kprintf(" uptime ticks and seconds since boot\n");
kprintf(" ps list kernel threads\n");
kprintf(" jobs list running background jobs\n");
kprintf(" export N=V set environment variable N to V (inherited by children)\n");
kprintf(" unset N remove environment variable N\n");
kprintf(" env list this shell's environment variables\n");
kprintf(" clear clear the screen\n");
kprintf(" $NAME (in a spawned program's arguments) expands to that variable\n");
kprintf(" $? (same place) expands to the last command's exit status\n");
kprintf(" NAME dir/*.ext (glob) expands to every real matching filename in dir\n");
kprintf(" NAME [ARGS] run /bin/NAME in the foreground, waits for it to exit\n");
kprintf(" NAME [ARGS] & run /bin/NAME in the background, prints its pid\n");
kprintf(" NAME < IN redirect NAME's stdin from file IN\n");
kprintf(" NAME <<< TEXT feed TEXT (plus a newline) to NAME's stdin\n");
kprintf(" NAME > OUT redirect NAME's stdout to file OUT (create/truncate)\n");
kprintf(" NAME >> OUT redirect NAME's stdout to file OUT (create/append)\n");
kprintf(" NAME 2> ERR redirect NAME's stderr to file ERR (create/truncate)\n");
kprintf(" NAME 2>> ERR redirect NAME's stderr to file ERR (create/append)\n");
kprintf(" A [ARGS] | B [ARGS] pipe A's stdout into B's stdin, waits for both\n");
}
static void printMem(void)
{
uint64 totalKib = pmmTotalBytes() / 1024;
uint64 usedKib = pmmUsedFrames() * frameSize / 1024;
uint64 freeKib = pmmFreeFrames() * frameSize / 1024;
kprintf("physical memory: %lu KiB total\n", totalKib);
kprintf(" frames: %lu total, %lu used, %lu free\n",
pmmTotalFrames(), pmmUsedFrames(), pmmFreeFrames());
kprintf(" used %lu KiB, free %lu KiB\n", usedKib, freeKib);
kprintf("heap: %lu KiB used of %lu KiB\n",
heapUsed() / 1024, heapTotal() / 1024);
}
static void printUptime(void)
{
uint64 ticks = pitTicks();
uint32 frequency = pitFrequency();
kprintf("uptime: %lu ticks, %lu seconds (at %u Hz)\n",
ticks, ticks / frequency, frequency);
}
static const char* threadStateName(ThreadState state)
{
switch (state) {
case threadUnused: return "unused";
case threadRunnable: return "runnable";
case threadRunning: return "running";
case threadSleeping: return "sleeping";
case threadDead: return "dead";
case threadBlocked: return "blocked";
case threadStopped: return "stopped";
default: return "?";
}
}
static void printThreads(void)
{
kprintf("id state name\n");
for (uint32 i = 0; i < threadCount(); i++) {
Thread* thread = threadAt(i);
const char* state = threadStateName(thread->state);
char marker = thread->id == currentThreadId() ? '*' : ' ';
kprintf("%c%u %s %s\n", marker, thread->id, state, thread->name);
}
}
static void buildPath(const char* argument, char* out)
{
vfsResolvePath(argument, out, pathCapacity);
}
/* One line of `ls` output for a single entry, real stat data (fs.h's
FbStat) already resolved by the caller -- shared by both the plain and
`-l` formats and by the "argument names a file, not a directory" case
below, so all three ever only format an FbStat one way. */
static void printLsEntry(bool longFormat, const FbStat* stat, const char* name)
{
char typeChar = stat->type == fsTypeDir ? 'd' : (stat->type == fsTypeFifo ? 'p' : 'f');
if (!longFormat) {
if (stat->type == fsTypeDir)
kprintf("%s/\n", name);
else
kprintf("%s %lu\n", name, stat->size);
return;
}
/* Deliberately prints type/size/name BEFORE the date: a needle that
greps for a known type+size+name substring (tools/ci-test.sh) then
never has to also match today's wall-clock date, which is real and
therefore different on every run. The real owner/group/other rwx
permission string (fs.h's fsModeOwner*, fsModeGroup*, fsModeOther*) is
appended AFTER the date for the same reason, in the other direction
-- every existing needle that greps a type+size+name+date-prefix
substring keeps matching unchanged, since nothing before it in the
line moved (only perm itself grew longer, from 3 chars to the full
9-char rwxrwxrwx real `ls -l` prints, now that a real gid exists to
make the group triad meaningful). */
char perm[10];
perm[0] = (stat->mode & fsModeOwnerRead) ? 'r' : '-';
perm[1] = (stat->mode & fsModeOwnerWrite) ? 'w' : '-';
perm[2] = (stat->mode & fsModeOwnerExec) ? 'x' : '-';
perm[3] = (stat->mode & fsModeGroupRead) ? 'r' : '-';
perm[4] = (stat->mode & fsModeGroupWrite) ? 'w' : '-';
perm[5] = (stat->mode & fsModeGroupExec) ? 'x' : '-';
perm[6] = (stat->mode & fsModeOtherRead) ? 'r' : '-';
perm[7] = (stat->mode & fsModeOtherWrite) ? 'w' : '-';
perm[8] = (stat->mode & fsModeOtherExec) ? 'x' : '-';
perm[9] = 0;
kprintf("%c %lu %s ", typeChar, stat->size, name);
if (stat->mtime == 0) {
/* tmpfs/dev entries never track time (fs.h's FbStat comment) --
an honest "no timestamp" marker instead of a fake 1970 date. */
kprintf("- %s\n", perm);
} else {
RtcDateTime dt;
rtcCivilFromEpoch((int64)stat->mtime, &dt);
kprintf("%04ld-%02ld-%02ld %02ld:%02ld:%02ld %s\n",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, perm);
}
}
/* `ls [-l] [PATH]` -- generalized over vfsOpen/vfsGetdents/vfsStat
(fs.h/vfs.h) instead of the old tmpfs-only vfsLookup/vfsChild/vfsNext
VfsNode-tree walk, so a directory under `/mnt` (SFS) can finally be
listed from the interactive shell at all (previously vfsLookup, which
only ever understood the in-memory tmpfs tree, silently returned "no
such path" for every SFS directory -- fstest.c/linktest.c could only
ever prove SFS directory listing through a raw userGetdents syscall,
never through this builtin). `-l` adds real stat data per entry (type,
size, mtime -- sfs.h's on-disk timestamps, FALA 39) the same way a real
`ls -l` would. */
/* Returns 0 on success, 1 on failure (real POSIX `ls` exit-status shape) --
feeds shell.c's new `&&`/`||` conditional chaining (execute()) the same
way a spawned program's own real exit code already does, see
shellSplitAndOr's own doc comment in shell.h. */
static int listDir(const char* argument)
{
bool longFormat = false;
if (argument[0] == '-' && argument[1] == 'l' && (argument[2] == 0 || argument[2] == ' ')) {
longFormat = true;
argument += 2;
while (*argument == ' ')
argument++;
}
char path[pathCapacity];
buildPath(argument, path);
FbStat stat;
if (vfsStat(path, &stat) != 0) {
kprintf("ls: no such path: %s\n", path);
return 1;
}
if (stat.type != fsTypeDir) {
const char* base = path;
for (const char* p = path; *p; p++) {
if (*p == '/')
base = p + 1;
}
printLsEntry(longFormat, &stat, base);
return 0;
}
int fd = vfsOpen(path, oReadOnly);
if (fd < 0) {
kprintf("ls: cannot open %s\n", path);
return 1;
}
uint64 pathLen = strlen(path);
if (pathLen >= pathCapacity)
pathLen = pathCapacity - 1;
bool needSlash = pathLen == 0 || path[pathLen - 1] != '/';
FbDirent entries[16];
int64 bytes;
while ((bytes = vfsGetdents(fd, entries, sizeof(entries))) > 0) {
int64 count = bytes / (int64)sizeof(FbDirent);
for (int64 i = 0; i < count; i++) {
char childPath[pathCapacity];
uint64 childLen = pathLen;
memcpy(childPath, path, childLen);
if (needSlash && childLen < pathCapacity - 1)
childPath[childLen++] = '/';
uint64 nameLen = strlen(entries[i].name);
if (childLen + nameLen >= pathCapacity)
nameLen = pathCapacity - 1 - childLen;
memcpy(childPath + childLen, entries[i].name, nameLen);
childPath[childLen + nameLen] = 0;
FbStat childStat;
if (vfsStat(childPath, &childStat) == 0)
printLsEntry(longFormat, &childStat, entries[i].name);
else
kprintf("%s ?\n", entries[i].name);
}
}
vfsClose(fd);
return 0;
}
/* Same "0 success, 1 failure" shape listDir above uses. */
static int catFile(const char* argument)
{
if (argument[0] == 0) {
kprintf("cat: missing file\n");
return 1;
}
char path[pathCapacity];
buildPath(argument, path);
VfsNode* node = vfsLookup(path);
if (!node) {
kprintf("cat: no such file: %s\n", path);
return 1;
}
if (vfsIsDir(node)) {
kprintf("cat: %s is a directory\n", path);
return 1;
}
const char* data = (const char*)vfsFileData(node);
uint64 size = vfsSize(node);
for (uint64 i = 0; i < size; i++)
kputchar(data[i]);
return 0;
}
static void echoWrite(char* text, const char* target)
{
char path[pathCapacity];
buildPath(target, path);
int fd = vfsOpen(path, oCreate | oWriteOnly | oTrunc);
if (fd < 0) {
kprintf("echo: cannot write %s\n", path);
return;
}
uint64 length = strlen(text);
vfsWrite(fd, text, length);
vfsWrite(fd, "\n", 1);
vfsClose(fd);
}
/* Same "0 success, 1 failure" shape listDir/catFile above use. */
static int makeDir(const char* argument)
{
if (argument[0] == 0) {
kprintf("mkdir: missing path\n");
return 1;
}
char path[pathCapacity];
buildPath(argument, path);
if (vfsMkdir(path) != 0) {
kprintf("mkdir: cannot create %s\n", path);
return 1;
}
return 0;
}
/* Real `rm`: refuses a directory outright (real rm without -r never
descends), matching vfsUnlink's own EISDIR-shaped refusal (vfs.c) --
the vfsStat check here exists only to give a friendlier message than
vfsUnlink's plain -1 would on its own, the actual enforcement is
vfsUnlink's, not this. */
static int removeFile(const char* argument)
{
if (argument[0] == 0) {
kprintf("rm: missing path\n");
return 1;
}
char path[pathCapacity];
buildPath(argument, path);
FbStat stat;
if (vfsStat(path, &stat) != 0) {
kprintf("rm: no such file: %s\n", path);
return 1;
}
if (stat.type == fsTypeDir) {
kprintf("rm: %s: is a directory\n", path);
return 1;
}
if (vfsUnlink(path) != 0) {
kprintf("rm: cannot remove %s\n", path);
return 1;
}
return 0;
}
/* Real `rmdir`: only ever removes an EMPTY directory, same split from `rm`
above that real rmdir(2)/unlink(2) have. */
static int removeDir(const char* argument)
{
if (argument[0] == 0) {
kprintf("rmdir: missing path\n");
return 1;
}
char path[pathCapacity];
buildPath(argument, path);
FbStat stat;
if (vfsStat(path, &stat) != 0) {
kprintf("rmdir: no such directory: %s\n", path);
return 1;
}
if (stat.type != fsTypeDir) {
kprintf("rmdir: %s: not a directory\n", path);
return 1;
}
if (vfsRmdir(path) != 0) {
kprintf("rmdir: %s: not empty\n", path);
return 1;
}
return 0;
}
/* Real `chmod MODE PATH`: MODE is parsed as octal (strtoul base 8, klib.h),
matching real chmod(1)'s numeric-mode convention -- "755" already means
0755, no leading 0 required. Both the owner AND other triads are
meaningful now that a real uid model exists (fs.h's fsModeOwner and
fsModeOther bits, vfs.c's vfsPermitted) -- refused outright unless the
caller owns the target or is uid 0 (vfsChmod's own vfsOwnerOrRoot check).
Symbolic modes (u+x, a=r, ...) are not supported, the same "numeric only"
scope every other new builtin in this shell has picked over a fuller
grammar it does not need. */
static int doChmod(const char* argument)
{
char modeText[16];
uint64 i = 0;
while (argument[i] && argument[i] != ' ' && i < sizeof(modeText) - 1) {
modeText[i] = argument[i];
i++;
}
modeText[i] = 0;
const char* rest = argument + i;
while (*rest == ' ')
rest++;
if (modeText[0] == 0 || rest[0] == 0) {
kprintf("chmod: usage: chmod MODE PATH\n");
return 1;
}
uint32 mode = (uint32)strtoul(modeText, nullptr, 8);
char path[pathCapacity];
buildPath(rest, path);
if (vfsChmod(path, mode) != 0) {
kprintf("chmod: cannot chmod %s\n", path);
return 1;
}
return 0;
}
/* Real `chown UID PATH`: UID is parsed as a plain decimal number (strtoul
base 10, klib.h), unlike chmod's octal MODE -- a uid is never a bitmask,
so there is no "leading digit means octal" convention to match. Refused
outright unless the caller is uid 0 (vfsChown's own root-only check) --
even the file's own owner may not give it away, matching real Unix's
_POSIX_CHOWN_RESTRICTED default. */
static int doChown(const char* argument)
{
char uidText[16];
uint64 i = 0;
while (argument[i] && argument[i] != ' ' && i < sizeof(uidText) - 1) {
uidText[i] = argument[i];
i++;
}
uidText[i] = 0;
const char* rest = argument + i;
while (*rest == ' ')
rest++;
if (uidText[0] == 0 || rest[0] == 0) {
kprintf("chown: usage: chown UID PATH\n");
return 1;
}
uint32 uid = (uint32)strtoul(uidText, nullptr, 10);
char path[pathCapacity];
buildPath(rest, path);
if (vfsChown(path, uid) != 0) {
kprintf("chown: cannot chown %s\n", path);
return 1;
}
return 0;
}
/* Real `chgrp GID PATH`: decimal like chown, not octal like chmod -- a gid
is never a bitmask either. Unlike chown (root-only, full stop), the
file's own owner may also call this, but only to set the group to THIS
shell's own current gid (vfsChgrp's own owner-may-only-name-their-own-gid
rule, see its doc comment) -- root may still set any gid. */
static int doChgrp(const char* argument)
{
char gidText[16];
uint64 i = 0;
while (argument[i] && argument[i] != ' ' && i < sizeof(gidText) - 1) {
gidText[i] = argument[i];
i++;
}
gidText[i] = 0;
const char* rest = argument + i;
while (*rest == ' ')
rest++;
if (gidText[0] == 0 || rest[0] == 0) {
kprintf("chgrp: usage: chgrp GID PATH\n");
return 1;
}
uint32 gid = (uint32)strtoul(gidText, nullptr, 10);
char path[pathCapacity];
buildPath(rest, path);
if (vfsChgrp(path, gid) != 0) {
kprintf("chgrp: cannot chgrp %s\n", path);
return 1;
}
return 0;
}
/* Same "0 success, 1 failure" shape as above. */
static int changeDir(const char* argument)
{
char path[pathCapacity];
if (argument[0] == 0)
memcpy(path, "/", 2);
else
buildPath(argument, path);
/* vfsChdir (vfs.h) also understands "/mnt" directories -- unlike the
vfsLookup+vfsIsDir check this used to do, which only ever walked the
VfsNode tree (ustar/tmpfs) and so could never actually cd into the
persistent on-disk filesystem at all. */
if (vfsChdir(path) != 0) {
kprintf("cd: no such directory: %s\n", path);
return 1;
}
return 0;
}
/* `export NAME=VALUE` -- real POSIX shell syntax, no quoting support
(matching every other built-in in this shell, e.g. doEcho below never
understands quotes either): the FIRST '=' splits `argument` into name
and value, everything after it becomes the value verbatim (including any
embedded spaces), the same "rest of the line, taken as-is" shape doEcho
already gives its own unredirected text. currentThreadSetEnv (sched.h)
sets it directly in THIS thread's own table -- the shell is a plain
kernel thread (shellRun's own comment above), so it needs no syscall to
reach its own state, exactly like changeDir calling vfsChdir directly
above. Any job spawned AFTER this call (jobctl.h, via threadCreateUser's
initialEnv) inherits a real copy of the new value. */
/* Same "0 success, 1 failure" shape listDir/catFile/makeDir/changeDir
above use. */
static int doExport(char* argument)
{
char* eq = argument;
while (*eq && *eq != '=')
eq++;
if (*eq != '=' || eq == argument) {
kprintf("export: usage: export NAME=VALUE\n");
return 1;
}
*eq = 0;
const char* value = eq + 1;
if (!currentThreadSetEnv(argument, value)) {
kprintf("export: cannot set %s (name/value too long, or too many variables)\n", argument);
return 1;
}
return 0;
}
/* `unset NAME` -- real POSIX shell syntax, removes `argument` from this
thread's own environment table via currentThreadUnsetEnv (sched.h),
the exact same "no syscall needed, shell is a plain kernel thread"
shape as doExport above. Silently succeeds whether or not `argument`
actually had an entry (matching real unsetenv(3)'s own no-op-success
semantics for an already-absent name) -- the usage error below is only
for the "no argument at all" case, not "name not found", and (same "0
success, 1 failure" shape listDir/catFile/makeDir/changeDir/doExport
above use) the only way this can ever return failure. */
static int doUnset(char* argument)
{
if (argument[0] == 0) {
kprintf("unset: usage: unset NAME\n");
return 1;
}
currentThreadUnsetEnv(argument);
return 0;
}
/* `env` -- lists every "NAME=VALUE" this shell thread currently holds
(currentThreadEnvCount/currentThreadEnvEntry, sched.h), the same raw
storage format doExport above writes and shellExpandArgv's $NAME lookups
read. */
static void printEnv(void)
{
int count = currentThreadEnvCount();
for (int i = 0; i < count; i++)
kprintf("env: %s\n", currentThreadEnvEntry(i));
}
static void doEcho(char* argument)
{
char* redirect = argument;
while (*redirect && *redirect != '>')
redirect++;
if (*redirect == '>') {
*redirect = 0;
char* target = redirect + 1;
while (*target == ' ')
target++;
char* textEnd = redirect;
while (textEnd > argument && textEnd[-1] == ' ')
textEnd--;
*textEnd = 0;
echoWrite(argument, target);
} else {
kprintf("%s\n", argument);
}
}
/* Upper bound on how many `$(...)` command substitutions execute() will
expand on one typed line -- the same "generous cap for one
lineCapacity-bounded line, not a claim real shells have such a limit"
reasoning shellMaxPipelineStages/shellMaxAndOrSegments already give
their own per-line caps. */
#define shellMaxCommandSubst 4
/* Expands every non-nested `$(cmd [args])` in `srcLine` into `outLine`,
left to right -- called from executeSingle below only once `&&`/`||`
(execute()'s own shellSplitAndOr, over the ORIGINAL typed line) and `|`
(executeSingle's own shellSplitPipelineStages, over THIS segment) have
already been resolved, so a literal `&&`/`||`/`|` byte sitting inside a
substituted command's own captured output can never be mistaken for a
real operator -- those are only ever recognized once, before any
expansion runs, exactly like a real shell's own grammar (operators are
parsed structurally first; expansion happens per-word afterward, never
the other way around).
Builds the result into a PRIVATE `work` copy first, splicing in place
there, rather than mutating `srcLine` itself: `srcLine` here is not
necessarily its own standalone buffer -- shellSplitAndOr/
shellSplitPipelineStages both split their input IN PLACE by writing NUL
terminators into ONE shared backing array (shell.h's own doc comments on
both), so a segment that still has ANOTHER segment's text sitting
immediately after its own NUL terminator would have that neighbor
silently corrupted by any in-place splice that grows past the original
text's own length. Working in a fully separate `work`/`outLine` buffer
makes that impossible regardless of how much the line grows or shrinks.
Each `$(...)` found (shellFindCommandSubst, shell.h) is run to
completion via jobctlCaptureOutput -- a REAL spawn, not a builtin (no fd
of its own to capture from, the same "no stage may be a builtin" scope
runPipelineStages already documents) -- with its own argv given
`$NAME`/`$?` expansion (shellExpandArgv) but nothing else: no wildcard
globs, no redirection, no pipes, no further `$(...)` nesting. Trailing
newlines are stripped from the captured output, matching a real shell's
own `$(...)`. Only ever scans PAST whatever it just spliced into `work`
(`scanFrom` below), never re-scanning a command's own captured output
for a further `$(` -- the same single-pass, no-second-expansion rule a
real shell gives command substitution's own result.
Returns true having written the fully expanded line into `outLine`
(unchanged from `srcLine` if there was nothing to expand), or false,
having already reported the error via kprintf and left `outLine`
untouched, on a malformed `$(` with no matching `)`, an unknown command,
a spliced result that would overflow lineCapacity, or more than
shellMaxCommandSubst substitutions on one line. */
static bool shellExpandCommandSubst(const char* srcLine, char* outLine, int outCap)
{
char work[lineCapacity];
snprintf(work, sizeof(work), "%s", srcLine);
int scanFrom = 0;
for (int iterations = 0; iterations < shellMaxCommandSubst; iterations++) {
int start, end;
char cmd[lineCapacity];
if (!shellFindCommandSubst(work + scanFrom, &start, &end, cmd, sizeof(cmd))) {
kprintf("shell: '$(' with no matching ')'\n");
return false;
}
if (start < 0) {
snprintf(outLine, (uint64)outCap, "%s", work);
return true;
}
start += scanFrom;
end += scanFrom;
char* word;
char* rest;
splitCommandWord(cmd, &word, &rest);
char* argv[shellMaxArgs];
int argc = 0;
argv[argc++] = word;
argc += shellTokenize(rest, &argv[argc], shellMaxArgs - argc);
char expandedArgv[shellMaxArgs][envEntryMax];
shellExpandArgv(argv + 1, argc - 1, expandedArgv + 1);
char binPath[binPathCapacity];
snprintf(binPath, sizeof(binPath), "/bin/%s", word);
char captured[lineCapacity];
int64 status;
bool spawned = jobctlCaptureOutput(binPath, (const char* const*)argv, argc,
captured, sizeof(captured), &status);
/* Deliberately discarded: real shells leave `$?` reflecting the
OUTER command that actually runs after substitution completes,
never the substituted command itself (executeSingle/execute
already set lastStatus from whatever this line goes on to
spawn). */
(void)status;
if (!spawned) {
kprintf("%s: command substitution failed (unknown command)\n", word);
return false;
}
int capturedLen = (int)strlen(captured);
while (capturedLen > 0 && captured[capturedLen - 1] == '\n')
captured[--capturedLen] = 0;
int tailLen = (int)strlen(work + end);
if (start + capturedLen + tailLen + 1 > lineCapacity) {
kprintf("shell: command substitution result too long\n");
return false;
}
memmove(work + start + capturedLen, work + end, (uint64)(tailLen + 1));
memcpy(work + start, captured, (uint64)capturedLen);
scanFrom = start + capturedLen;
}
kprintf("shell: too many '$(' substitutions (max %d)\n", shellMaxCommandSubst);
return false;
}
/* Runs exactly one command-or-pipeline (never itself another `&&`/`||`
chain -- shellSplitAndOr has already peeled those apart before this is
ever called, see execute() below) and returns its real exit status: a
builtin's own 0/1 (listDir/catFile/makeDir/changeDir/doExport/doUnset),
0 for every other builtin (none of them has a documented failure case),
or whatever runProgram/runPipelineStages themselves return for a
spawned command or pipeline. This used to be the entire body of
execute() itself, before shell.c had any notion of chaining multiple
commands' exit statuses together. */
static int64 executeSingle(char* line)
{
if (line[0] == 0)
return 0;
char* pipeStages[shellMaxPipelineStages];
int pipeStageCount = shellSplitPipelineStages(line, pipeStages, shellMaxPipelineStages);
if (pipeStageCount == -1) {
kprintf("pipeline: too many '|' stages (max %d)\n", shellMaxPipelineStages);
return shellExecNotFound;
}
if (pipeStageCount > 0)
return runPipelineStages(pipeStages, pipeStageCount);
/* `$(cmd)` command substitution (shellExpandCommandSubst above) only
ever runs for a single, non-piped command -- an honestly-scoped
limitation matching this file's own history of landing a feature for
the plain case first and generalizing to pipeline stages in a later
fala (`<<<` here-strings took the same path: jobctlSpawnForegroundHereString
landed before jobctlSpawnPipedHereString ever fed a pipeline's first
stage). `|` has already been ruled out above (pipeStageCount == 0),
so nothing here can still be misread as a pipe. */
char expandedLine[lineCapacity];
if (!shellExpandCommandSubst(line, expandedLine, sizeof(expandedLine)))
return shellExecNotFound;
line = expandedLine;
char* argument = line;
while (*argument && *argument != ' ')
argument++;
if (*argument == ' ') {
*argument = 0;
argument++;
}
while (*argument == ' ')
argument++;
if (strcmp(line, "help") == 0) {
printHelp();
return 0;
} else if (strcmp(line, "echo") == 0) {
doEcho(argument);
return 0;
} else if (strcmp(line, "ls") == 0)
return listDir(argument);
else if (strcmp(line, "cat") == 0)
return catFile(argument);
else if (strcmp(line, "mkdir") == 0)
return makeDir(argument);
else if (strcmp(line, "rm") == 0)
return removeFile(argument);
else if (strcmp(line, "rmdir") == 0)
return removeDir(argument);
else if (strcmp(line, "chmod") == 0)
return doChmod(argument);
else if (strcmp(line, "chown") == 0)
return doChown(argument);
else if (strcmp(line, "chgrp") == 0)
return doChgrp(argument);
else if (strcmp(line, "cd") == 0)
return changeDir(argument);
else if (strcmp(line, "pwd") == 0) {
char path[pathCapacity];
vfsGetcwd(path, sizeof(path));
kprintf("%s\n", path);
return 0;
}
else if (strcmp(line, "whoami") == 0) {
kprintf("%u\n", currentThreadUid());
return 0;
}
else if (strcmp(line, "mem") == 0) {
printMem();
return 0;
} else if (strcmp(line, "uptime") == 0) {
printUptime();
return 0;
} else if (strcmp(line, "ps") == 0) {
printThreads();
return 0;
} else if (strcmp(line, "jobs") == 0) {
listBackgroundJobs();
return 0;
} else if (strcmp(line, "export") == 0)
return doExport(argument);
else if (strcmp(line, "env") == 0) {
printEnv();
return 0;
} else if (strcmp(line, "unset") == 0)
return doUnset(argument);
else if (strcmp(line, "clear") == 0) {
vgaClear();
return 0;
} else
return runProgram(line, argument);
}
/* Splits `line` at every top-level `&&`/`||` (shellSplitAndOr, shell.h)
and runs each segment left to right through executeSingle above,
short-circuiting exactly like a real shell: a segment preceded by `&&`
only runs if the PREVIOUS segment that actually ran (not necessarily the
lexically previous one -- a skipped segment leaves `status` completely
unchanged) exited 0, one preceded by `||` only runs if that same status
was nonzero. This is real conditional chaining WITHIN a single typed
line (`cmd1 && cmd2 || cmd3`, the classic shell if-then-else idiom).
`lastStatus` (this file's own file-scope static above) is updated right
after every executeSingle() call below, chained or not -- a real shell's
own `$?` updates after EVERY simple command, including mid-chain (so
`false || echo $?` sees `false`'s own status while evaluating `echo`'s
argv, not some later value), and survives PAST this function returning,
so a later, completely separate typed line can still read it via
shellExpandToken's own `$?` handling -- closing the "no persistent `$?`
across separately typed lines" gap this file used to document here as a
deliberate limitation. A SKIPPED segment leaves `lastStatus` untouched
exactly like it already leaves the local chaining `status` untouched. */
static void execute(char* line)
{
if (line[0] == 0)
return;
char* andOrSegments[shellMaxAndOrSegments];
ShellAndOrOp andOrOps[shellMaxAndOrSegments - 1];
int andOrCount = shellSplitAndOr(line, andOrSegments, andOrOps, shellMaxAndOrSegments);
if (andOrCount == -1) {
kprintf("shell: too many '&&'/'||' segments (max %d)\n", shellMaxAndOrSegments);
return;
}
if (andOrCount == 0) {
lastStatus = executeSingle(line);
return;
}
int64 status = 0;
for (int i = 0; i < andOrCount; i++) {
if (i > 0) {
bool shouldRun = (andOrOps[i - 1] == shellAndOrAnd) ? (status == 0) : (status != 0);
if (!shouldRun)
continue;
}
status = executeSingle(andOrSegments[i]);
lastStatus = status;
}
}
void shellRun(void)
{
char line[lineCapacity];
uint32 length = 0;
/* The shell is its own independent console reader (keyboard.h), same as
every ring-3 process's fd 0 -- a keystroke keyboardHandleScancode
decodes is broadcast to every open reader, this one included, so the
shell keeps working unchanged even while other processes are also
blocked reading fd 0 (see the consoleReaderA/consoleReaderB live boot
demo in kmain.c). Plain kernel-thread context (not a syscall
handler), so the blocking helper's real halt() loop is safe here,
exactly like pipeReadBlocking's callers. */
int reader = keyboardOpenReader();
if (reader == consoleReaderInvalid) {
kprintf("[shell] console reader table full, shell has no input\n");
for (;;)
halt();
}
kprintf("\nfrostbyte shell ready. type 'help'.\n");
kprintf("frostbyte> ");
for (;;) {
char c;
keyboardReaderReadBlocking(reader, &c, 1);
if (c == '\n') {
kputchar('\n');
line[length] = 0;
execute(line);
length = 0;
reapFinishedBackgroundJobs();
kprintf("frostbyte> ");
} else if (c == '\b') {
if (length > 0) {
length--;
kprintf("\b \b");
}
} else if (length < lineCapacity - 1) {
line[length++] = c;
kputchar(c);
}
}
}