1734 lines
67 KiB
C
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);
|
|
}
|
|
}
|
|
}
|