feat: extend shell with execute

This commit is contained in:
dualfroz committed 2026-09-08 13:23:34 +02:00
1 parent ebdc94d031
commit c885f33d76
2 files changed
+178 -17

No files matched your search

+137 -17
View File
@@ -1545,7 +1545,29 @@ static bool shellExpandCommandSubst(const char* srcLine, char* outLine, int outC
static int64 executeSingle(char* line)
{
if (line[0] == 0)
return;
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 != ' ')
@@ -1557,30 +1579,111 @@ static int64 executeSingle(char* line)
while (*argument == ' ')
argument++;
if (strcmp(line, "help") == 0)
if (strcmp(line, "help") == 0) {
printHelp();
else if (strcmp(line, "echo") == 0)
return 0;
} else if (strcmp(line, "echo") == 0) {
doEcho(argument);
else if (strcmp(line, "ls") == 0)
listDir(argument);
return 0;
} else if (strcmp(line, "ls") == 0)
return listDir(argument);
else if (strcmp(line, "cat") == 0)
catFile(argument);
return catFile(argument);
else if (strcmp(line, "mkdir") == 0)
makeDir(argument);
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)
changeDir(argument);
else if (strcmp(line, "pwd") == 0)
kprintf("%s\n", cwd);
else if (strcmp(line, "mem") == 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();
else if (strcmp(line, "uptime") == 0)
return 0;
} else if (strcmp(line, "uptime") == 0) {
printUptime();
else if (strcmp(line, "ps") == 0)
return 0;
} else if (strcmp(line, "ps") == 0) {
printThreads();
else if (strcmp(line, "clear") == 0)
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();
else
kprintf("unknown command: %s (try 'help')\n", line);
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)
@@ -1588,17 +1691,34 @@ 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 = keyboardReadChar();
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) {
+41
View File
@@ -0,0 +1,41 @@
#include "syscall.h"
/*
* Shared signal-delivery trampoline (see signal.h/signal.c). This is NOT
* entered like a normal function call from C -- signal.c's
* sigConsumePending rewrites a thread's own saved trapframe so that
* iretq lands ring 3 execution directly at sigTrampolineStart, with %rax
* already holding the handler's address and %rdi already holding the
* signal number (the handler's sole SysV argument), exactly as if the
* kernel itself had performed `call *%rax` with that argument set up.
*
* Position independent by construction: every instruction here is either
* PC-relative (the final backward jmp) or references no address at all
* (call through a register, an immediate syscall number, int $0x80) -- so
* sigInstallTrampoline can copy these bytes to any physical frame and map
* that frame at any virtual address (sigTrampolineVirtualAddr) and it
* still runs correctly, unlike this same code linked and executed in
* place inside the kernel's own (very different) virtual address range.
*/
.section .text
.code64
.global sigTrampolineStart
.global sigTrampolineEnd
sigTrampolineStart:
call *%rax
movl $sysSigReturn, %eax
int $0x80
/* Only reached if sysSigReturn somehow returned instead of restoring the
pre-signal context and resuming it (a kernel bug, not a state a correct
sysOnSigReturn can produce) -- park here rather than run off into
whatever garbage happens to follow this page. */
sigTrampolineHang:
hlt
jmp sigTrampolineHang
sigTrampolineEnd:
.section .note.GNU-stack, "", @progbits