| git.druid.rocks | index | druid520 | kaboom | src/ | user/ | sh.nsc |
src/user/sh.nsc
include "syscalls.nsh";
include "lib.nsh";
include "string.nsh";
include "mem.nsh";
include "stdio.nsh";
/* minimalized csh+ash: one builtin set (cd/pwd/exit), everything else
* exec'd via kfs's flat-ish namespace + the cwd model kfs_cd/kfs_pwd
* already track kernel-side. no quoting, no escaping, no pipes/redirs,
* no job control -- splits on bare spaces only, same "ship the real
* minimal version" pattern as every other program here. */
void
do_pwd(void)
{
i8 buf[256];
u64 len;
len = sys_pwd(&buf[0]);
sys_write(1, &buf[0], len); /* dynamic content, not a literal -- stays sys_write */
putchar((u8)10);
stdio_flush();
}
/* splits line (null-terminated, in place -- either the interactive
* input buffer below or one line of a running script) on bare spaces
* and runs it exactly like a typed command: the builtins (cd/pwd/
* exit), or sys_exec otherwise. shared by both the interactive loop
* and run_script so a script behaves identically to typing the same
* lines by hand -- there's no separate "script dialect" here at all.
* returns 0 if this line was "exit" (the caller should stop), 1
* otherwise. */
i32
run_line(ptr line)
{
ptr words[16];
i32 nwords;
u64 i;
i64 result;
nwords = 0;
i = 0;
while(ptr_byte_at(line, i) != (u8)0)
{
while(ptr_byte_at(line, i) == (u8)32)
{
i = i + (u64)1;
}
if(ptr_byte_at(line, i) != (u8)0)
{
if(nwords < 16)
{
words[nwords] = line + i;
nwords = nwords + 1;
}
while(ptr_byte_at(line, i) != (u8)32 && ptr_byte_at(line, i) != (u8)0)
{
i = i + (u64)1;
}
if(ptr_byte_at(line, i) == (u8)32)
{
mem_byte_set(line, i, (u8)0);
i = i + (u64)1;
}
}
}
if(nwords == 0)
{
return 1; /* empty line: nothing to do */
}
if(strcmp(words[0], "exit") == 0)
{
return 0;
}
if(strcmp(words[0], "cd") == 0)
{
if(nwords >= 2)
{
if(sys_cd(words[1], strlen(words[1])) == 0)
{
print_err(2, "cd", "no such directory");
}
}
else
{
sys_cd("/", (u64)1);
}
return 1;
}
if(strcmp(words[0], "pwd") == 0)
{
do_pwd();
return 1;
}
result = sys_exec(words[0], strlen(words[0]), (i64)nwords, (ptr)&words[0]);
if(result == (i64)(-1))
{
/* NOT print_err/sys_errno here, deliberately: exec's own -1 is
* already documented as ambiguous (idt.nsc's own comment on
* syscall 7) -- a real exec failure and a child program that
* legitimately returned -1 as its own exit code are
* indistinguishable from this return value alone, so
* sys_errno's value at this point can't be trusted to mean
* "why sys_exec itself failed" either (a child that ran fine
* and made its OWN syscalls would have left sys_errno at
* whatever ITS last syscall set it to, not sys_exec's). */
fputs(2, "sh: command not found\n");
}
return 1;
}
/* runs a script file one command per line -- blank lines and lines
* starting with '#' (the shebang line itself included, if present)
* are skipped, everything else runs through run_line exactly as if it
* had been typed interactively. no pipes, no redirection, no control
* flow -- "super simple, just commands," which is genuinely the whole
* feature. this is what a shebang script actually runs under: the
* kernel's own sys_exec (exec.nsc) notices a file starting with "#!"
* isn't a real elf and re-execs whatever interpreter the line names
* (in practice, always sh, kaboom's only one) with the script's path
* as argv[1] -- which is exactly the argc>=2 case main() below checks
* for. typing "sh somescript" directly at the prompt reaches the same
* place a different way: "sh" is a perfectly ordinary, real elf, so
* sys_exec loads and runs it like any other program (into a fresh
* frame under sh's own window, since this sh is still live there --
* see exec.nsc's sys_exec), and IT sees argc>=2. */
void
run_script(ptr path, u64 path_len)
{
ptr buf;
i64 n;
i32 fd;
u64 i;
u64 line_start;
u8 ch;
fd = sys_open(path, path_len);
if(fd < 0)
{
print_err(2, "sh", "script not found");
return;
}
/* read_all: the whole script regardless of size, nul-terminated
* after the last byte (the trailing-line case below relies on
* that). */
buf = read_all(fd, &n);
sys_close(fd);
if(buf == (ptr)0)
{
print_err(2, "sh", "script read failed");
return;
}
i = 0;
line_start = 0;
while((i64)i < n)
{
ch = ptr_byte_at(buf, i);
if(ch == (u8)10)
{
mem_byte_set(buf, i, (u8)0);
if(ptr_byte_at(buf, line_start) != (u8)35) /* '#' */
{
if(run_line(buf + line_start) == 0)
{
return; /* "exit" */
}
}
line_start = i + (u64)1;
}
i = i + (u64)1;
}
/* a trailing line with no final newline still runs. */
if(line_start < (u64)n && ptr_byte_at(buf, line_start) != (u8)35)
{
run_line(buf + line_start);
}
}
global i32
main(i32 argc, ptr argv)
{
i8 line[256];
i64 linelen;
ptr path;
if(argc >= 2)
{
/* invoked with a script path (see run_script's own note on the
* two ways this happens) -- run it to completion and exit,
* same as a real "sh script.sh" would, instead of ever
* entering the interactive loop below. */
path = argv_get(argv, 1);
run_script(path, strlen(path));
return 0;
}
sys_vga_clear(); /* vga and serial both, see idt.nsc's syscall 16 -- a
* clean screen for the interactive prompt, not for
* script mode (which already returned above). */
while(1)
{
fputs(1, "$ ");
/* at most 255 bytes, leaving line[255] for the terminator.
* sys_read(0) only ever stops short of its limit at a newline,
* so a full 255 bytes with no newline means the line is longer
* than the buffer: the rest is read and thrown away up to the
* real newline, and the whole line is rejected. before, that
* rest stayed queued and ran as its own separate command on
* the next read, and the terminator went to line[256], one
* byte past the buffer. running a truncated prefix instead
* wouldn't be safe either ("rm longname" cut short can name a
* different file). */
linelen = sys_read(0, &line[0], (u64)255);
if(linelen <= (i64)0)
{
line[0] = 0;
}
else if(ptr_byte_at(&line[0], (u64)(linelen - (i64)1)) == (u8)10)
{
line[linelen - (i64)1] = 0;
}
else
{
while(linelen == (i64)255 && ptr_byte_at(&line[0], (u64)254) != (u8)10)
{
linelen = sys_read(0, &line[0], (u64)255);
if(linelen <= (i64)0)
{
linelen = 0; /* eof/error: nothing left to discard */
}
else if(ptr_byte_at(&line[0], (u64)(linelen - (i64)1)) == (u8)10)
{
linelen = 0; /* reached the real end of the line */
}
}
line[0] = 0;
fputs(2, "sh: line too long, ignored\n");
}
if(run_line(&line[0]) == 0)
{
return 0;
}
}
return 0;
}