aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-24 12:48:42 +0200
committeruser <user@clank>2026-09-24 12:48:42 +0200
commit62c392178e774c09e42927cfdd0d4daebcd185ce (patch)
tree3347624de15c81a80a5e9bad395cbdebf288be61
parentkernel: stat/fsstat/isatty syscalls, and O_APPEND (diff)
downloadgbos-62c392178e774c09e42927cfdd0d4daebcd185ce.tar.gz
gbos-62c392178e774c09e42927cfdd0d4daebcd185ce.tar.xz
gbos-62c392178e774c09e42927cfdd0d4daebcd185ce.zip
sh: run scripts, and >> redirection
`sh < file` already executed the file - and then powered the Game Boy off, because the read loop treated EOF as "the machine is done". That is right for init and wrong for everything else, so scripts were unusable. A shell now asks isatty(): with stdin on a file it prints no prompt, does not echo the script back, and exits at EOF. Only pid 1 on the console still powers down. init also runs /rc at boot if the cart has one, so a Game Boy can come up with its own startup script - verified across a real power cycle (write /rc, poweroff, reboot the same cart, watch init run it). `>>` appends instead of truncating, which is what makes writing that script on the device possible in the first place.
-rw-r--r--usr/sh.c219
1 files changed, 165 insertions, 54 deletions
diff --git a/usr/sh.c b/usr/sh.c
index a259b81..a280b98 100644
--- a/usr/sh.c
+++ b/usr/sh.c
@@ -3,7 +3,7 @@
( > file, < file ) and pipes ( cmd1 | cmd2, via a temp file ).
Operators must be space-separated (e.g. cat readme > out ). */
-#define ARGV0 ((char *)0xA000) /* where a child reads its command line */
+#define ARGV0 ((char *)0xA000) /* where a child reads its command line */
/* split s into space-separated tokens (NUL-terminated in place). */
static unsigned char tokenize(char *s, char **tok, unsigned char max) {
@@ -18,27 +18,35 @@ static unsigned char tokenize(char *s, char **tok, unsigned char max) {
}
static unsigned char isop(char *t, char c) { return t[0] == c && t[1] == 0; }
-static unsigned char iscmd(char *t, char a, char b) {
- return t[0] == a && t[1] == b && t[2] == 0;
-}
+static unsigned char iscmd(char *t, char a, char b) { return t[0] == a && t[1] == b && t[2] == 0; }
/* keep the prompt's path string roughly in sync with chdir(). */
static void set_cwd(char *cwd, char *arg) {
char *d, *s, *last;
- if (arg[0] == '/') { /* absolute */
- d = cwd; s = arg;
+ if (arg[0] == '/') { /* absolute */
+ d = cwd;
+ s = arg;
while (*s) *d++ = *s++;
*d = 0;
} else if (arg[0] == '.' && arg[1] == '.' && arg[2] == 0) {
- last = cwd; d = cwd;
- while (*d) { if (*d == '/') last = d; d++; }
- if (last == cwd) { cwd[0] = '/'; cwd[1] = 0; } else *last = 0;
+ last = cwd;
+ d = cwd;
+ while (*d) {
+ if (*d == '/') last = d;
+ d++;
+ }
+ if (last == cwd) {
+ cwd[0] = '/';
+ cwd[1] = 0;
+ } else *last = 0;
} else if (arg[0] == '.' && arg[1] == 0) {
/* stay */
- } else { /* relative: append */
- d = cwd; while (*d) d++;
+ } else { /* relative: append */
+ d = cwd;
+ while (*d) d++;
if (!(cwd[0] == '/' && cwd[1] == 0)) *d++ = '/';
- s = arg; while (*s) *d++ = *s++;
+ s = arg;
+ while (*s) *d++ = *s++;
*d = 0;
}
}
@@ -49,7 +57,7 @@ static void set_cmdline(char **tok, unsigned char start, unsigned char end) {
unsigned char i;
t = tok[start];
while (*t) *p++ = *t++;
- *p++ = 0; /* NUL after the command word */
+ *p++ = 0; /* NUL after the command word */
for (i = start + 1; i < end; i++) {
if (i > start + 1) *p++ = ' ';
t = tok[i];
@@ -60,11 +68,14 @@ static void set_cmdline(char **tok, unsigned char start, unsigned char end) {
/* run tok[start..end): parse its own >/< redirects, honor forced in/out.
If bg, launch in the background (don't wait; leave fds to the child). */
-static void run(char **tok, unsigned char start, unsigned char end,
- unsigned char infd, unsigned char outfd, unsigned char bg) {
+static void run(char **tok, unsigned char start, unsigned char end, unsigned char infd,
+ unsigned char outfd, unsigned char bg) {
unsigned char i, cmdend = end, myin = NOFD, myout = NOFD, id, pid;
for (i = start; i < end; i++) {
- if (isop(tok[i], '>')) {
+ if (iscmd(tok[i], '>', '>')) {
+ if (i + 1 < end) myout = open(tok[i + 1], O_APPEND);
+ if (i < cmdend) cmdend = i;
+ } else if (isop(tok[i], '>')) {
if (i + 1 < end) myout = open(tok[i + 1], O_WRITE);
if (i < cmdend) cmdend = i;
} else if (isop(tok[i], '<')) {
@@ -73,7 +84,12 @@ static void run(char **tok, unsigned char start, unsigned char end,
}
}
id = lookup(tok[start]);
- if (id == NOFD) { puts(tok[start]); puts(": not found"); nl(); goto done; }
+ if (id == NOFD) {
+ puts(tok[start]);
+ puts(": not found");
+ nl();
+ goto done;
+ }
set_cmdline(tok, start, cmdend);
pid = fork();
if (pid == 0) {
@@ -83,12 +99,21 @@ static void run(char **tok, unsigned char start, unsigned char end,
else if (outfd != NOFD) setout(outfd);
exec(id);
}
- if (bg) { putc('['); putu(pid); puts("]"); nl(); return; } /* background */
+ if (bg) {
+ putc('[');
+ putu(pid);
+ puts("]");
+ nl();
+ return;
+ } /* background */
/* wait for THIS child; report any background jobs that finish meanwhile */
for (;;) {
unsigned char r = wait();
if (r == pid || r == NOFD) break;
- putc('['); putu(r); puts(" done]"); nl();
+ putc('[');
+ putu(r);
+ puts(" done]");
+ nl();
}
done:
if (myin != NOFD) close(myin);
@@ -99,26 +124,39 @@ done:
kernel pipes, launch them ALL concurrently, then wait for them. */
static void run_pipeline(char **tok, unsigned char nt) {
unsigned char pids[8], npid = 0;
- unsigned char cfd[8], ncfd = 0; /* file-redirect fds; closed after all exit */
+ unsigned char cfd[8], ncfd = 0; /* file-redirect fds; closed after all exit */
unsigned char start = 0, prev_r = NOFD;
for (;;) {
unsigned char end = start, last, i, cmdend, fin = NOFD, fout = NOFD;
unsigned char my_out = NOFD, next_r = NOFD, id, pid;
while (end < nt && !isop(tok[end], '|')) end++;
last = (end >= nt);
- if (!last) { /* pipe from this stage to the next */
+ if (!last) { /* pipe from this stage to the next */
unsigned char pr = pipe();
- if (pr != NOFD) { next_r = pr; my_out = pr + 1; }
+ if (pr != NOFD) {
+ next_r = pr;
+ my_out = pr + 1;
+ }
}
- cmdend = end; /* this stage's own >/< redirects */
+ cmdend = end; /* this stage's own >/< redirects */
for (i = start; i < end; i++) {
- if (isop(tok[i], '>')) { if (i + 1 < end) fout = open(tok[i+1], O_WRITE); if (i < cmdend) cmdend = i; }
- else if (isop(tok[i], '<')) { if (i + 1 < end) fin = open(tok[i+1], O_READ); if (i < cmdend) cmdend = i; }
+ if (iscmd(tok[i], '>', '>')) {
+ if (i + 1 < end) fout = open(tok[i + 1], O_APPEND);
+ if (i < cmdend) cmdend = i;
+ } else if (isop(tok[i], '>')) {
+ if (i + 1 < end) fout = open(tok[i + 1], O_WRITE);
+ if (i < cmdend) cmdend = i;
+ } else if (isop(tok[i], '<')) {
+ if (i + 1 < end) fin = open(tok[i + 1], O_READ);
+ if (i < cmdend) cmdend = i;
+ }
}
id = lookup(tok[start]);
if (id == NOFD) {
- puts(tok[start]); puts(": not found"); nl();
- if (prev_r != NOFD) close(prev_r); /* orphaned pipe ends -> EOF/EPIPE */
+ puts(tok[start]);
+ puts(": not found");
+ nl();
+ if (prev_r != NOFD) close(prev_r); /* orphaned pipe ends -> EOF/EPIPE */
if (my_out != NOFD) close(my_out);
if (fin != NOFD) close(fin);
if (fout != NOFD) close(fout);
@@ -126,8 +164,10 @@ static void run_pipeline(char **tok, unsigned char nt) {
set_cmdline(tok, start, cmdend);
pid = fork();
if (pid == 0) {
- if (fin != NOFD) setin(fin); else if (prev_r != NOFD) setin(prev_r);
- if (fout != NOFD) setout(fout); else if (my_out != NOFD) setout(my_out);
+ if (fin != NOFD) setin(fin);
+ else if (prev_r != NOFD) setin(prev_r);
+ if (fout != NOFD) setout(fout);
+ else if (my_out != NOFD) setout(my_out);
exec(id);
}
if (npid < 8) pids[npid++] = pid;
@@ -138,65 +178,136 @@ static void run_pipeline(char **tok, unsigned char nt) {
if (last) break;
start = end + 1;
}
- { unsigned char got = 0; /* wait for all stages */
- while (got < npid) {
- unsigned char r = wait(), j, mine = 0;
- if (r == NOFD) break;
- for (j = 0; j < npid; j++) if (pids[j] == r) { mine = 1; break; }
- if (mine) got++;
- else { putc('['); putu(r); puts(" done]"); nl(); }
- }
+ {
+ unsigned char got = 0; /* wait for all stages */
+ while (got < npid) {
+ unsigned char r = wait(), j, mine = 0;
+ if (r == NOFD) break;
+ for (j = 0; j < npid; j++)
+ if (pids[j] == r) {
+ mine = 1;
+ break;
+ }
+ if (mine) got++;
+ else {
+ putc('[');
+ putu(r);
+ puts(" done]");
+ nl();
+ }
+ }
+ }
+ {
+ unsigned char k;
+ for (k = 0; k < ncfd; k++) close(cfd[k]);
}
- { unsigned char k; for (k = 0; k < ncfd; k++) close(cfd[k]); }
}
void main(void) {
char line[80];
char *tok[16];
char cwd[40];
- cwd[0] = '/'; cwd[1] = 0;
+ /* A shell whose stdin is a FILE is running a script: no prompt, no echo
+ of the script text, and end-of-input just ends the shell. Only init
+ (pid 1, on the console) treats EOF as "the machine is done". */
+ unsigned char tty = isatty();
+ unsigned char isinit = (unsigned char)(getpid() == 1);
+ cwd[0] = '/';
+ cwd[1] = 0;
/* bring up networking in the BACKGROUND: dhcp's recv blocks a long time when
no server answers, and wait()ing on it would leave the screen stuck at
"dhcp: discovering" with no prompt. Fork without waiting so the prompt is
immediate; the lease (or "no offer") lands a moment later and the job is
reaped by the loop below. */
- {
+ if (isinit) {
unsigned char d = lookup("dhcp");
if (d != NOFD && fork() == 0) exec(d);
}
+ /* init runs /rc if it is there: a boot script, now that a shell fed from
+ a file behaves itself. Wait for it, so its output lands before the
+ first prompt. */
+ if (isinit) {
+ unsigned char fd = open("/rc", O_READ);
+ if (fd <= 3) {
+ unsigned char id = lookup("sh");
+ if (id != NOFD && fork() == 0) {
+ setin(fd);
+ exec(id);
+ }
+ wait();
+ close(fd);
+ }
+ }
for (;;) {
unsigned char nt, i, pipepos, c, bg;
/* reap finished background jobs */
- { unsigned char p; while ((p = reap())) { putc('['); putu(p); puts(" done]"); nl(); } }
- puts(cwd); puts("# ");
- /* read a line (echoing), stop at newline; exit on EOF */
+ {
+ unsigned char p;
+ while ((p = reap())) {
+ putc('[');
+ putu(p);
+ puts(" done]");
+ nl();
+ }
+ }
+ if (tty) {
+ puts(cwd);
+ puts("# ");
+ }
+ /* read a line (echoing), stop at newline; exit on EOF.
+ EOF means "the machine is done" only for pid 1 - init's console has
+ nowhere else to go. Any other shell is running a script
+ (`sh < file`, the child of a pipe, ...) and must simply exit, or
+ reaching the end of a script powers the Game Boy off. */
i = 0;
for (;;) {
c = readc();
- if (c == EOF) poweroff();
- if (c == '\r' || c == '\n') { nl(); break; }
- if (c == 8 || c == 127) { /* backspace: drop last char */
- if (i > 0) { i--; putc(8); }
+ if (c == EOF) {
+ if (tty && isinit) poweroff();
+ sexit(0);
+ }
+ if (c == '\r' || c == '\n') {
+ if (tty) nl();
+ break;
+ }
+ if (c == 8 || c == 127) { /* backspace: drop last char */
+ if (i > 0) {
+ i--;
+ if (tty) putc(8);
+ }
continue;
}
- if (i < 79) { line[i++] = c; putc(c); }
+ if (i < 79) {
+ line[i++] = c;
+ if (tty) putc(c);
+ }
}
line[i] = 0;
nt = tokenize(line, tok, 16);
if (nt == 0) continue;
- if (iscmd(tok[0], 'c', 'd')) { /* cd builtin */
+ if (iscmd(tok[0], 'c', 'd')) { /* cd builtin */
char *arg = (nt >= 2) ? tok[1] : "/";
if (chdir(arg) == 0) set_cwd(cwd, arg);
- else { puts("cd: no such directory"); nl(); }
+ else {
+ puts("cd: no such directory");
+ nl();
+ }
continue;
}
- if ((tok[0][0] == 'e' && tok[0][1] == 'x') ||
- (tok[0][0] == 'q' && tok[0][1] == 'u')) poweroff();
+ if ((tok[0][0] == 'e' && tok[0][1] == 'x') || (tok[0][0] == 'q' && tok[0][1] == 'u'))
+ poweroff();
bg = 0;
- if (isop(tok[nt - 1], '&')) { bg = 1; nt--; if (nt == 0) continue; }
+ if (isop(tok[nt - 1], '&')) {
+ bg = 1;
+ nt--;
+ if (nt == 0) continue;
+ }
pipepos = 0;
for (i = 1; i < nt; i++)
- if (isop(tok[i], '|')) { pipepos = i; break; }
+ if (isop(tok[i], '|')) {
+ pipepos = i;
+ break;
+ }
if (pipepos) run_pipeline(tok, nt);
else run(tok, 0, nt, NOFD, NOFD, bg);
}