diff options
| author | user <user@clank> | 2026-09-24 12:48:51 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-24 12:48:51 +0200 |
| commit | 65b53de14dc6918db39b6ee6dc032f805182daad (patch) | |
| tree | 6af7b52262321a3cbe11e1d472ca2938df2fc37e | |
| parent | sh: run scripts, and >> redirection (diff) | |
| download | gbos-65b53de14dc6918db39b6ee6dc032f805182daad.tar.gz gbos-65b53de14dc6918db39b6ee6dc032f805182daad.tar.xz gbos-65b53de14dc6918db39b6ee6dc032f805182daad.zip | |
usr: ls -l, df, mv, tail, tee, sleep, help, poweroff
Filling in the core utilities the box was missing:
- ls -l: type, links and size, now that stat() exists. The screen is 40
columns, so it is "d 1 256 name", not the GNU sprawl. Sizes were
previously unknowable except one file at a time with wc -c.
- df: blocks and inodes, used and free, on the 32 KiB cart disk.
- mv: no rename syscall, so it is cp + rm - and the copy has to finish before
the source goes, or a failure loses the file.
- tail [-n N]: no seek syscall, so it streams the input through a ring of N
lines rather than jumping to the end. N is capped by that ring.
- tee: the missing half of a pipeline - `cat big | tee copy | wc -l`.
- sleep: the thing every script needs; the kernel counts 1/64 s ticks.
- help: lists every command the shell knows, walked out of the kernel's own
name table. The box previously had no way to tell you what it could do.
- poweroff: the only safe way to stop a gbos that has data on it. The cart's
battery RAM is written when the emulator exits cleanly, so killing it loses
the whole filesystem - it comes back "formatted". Found while testing /rc.
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | src/programs.asm | 49 | ||||
| -rw-r--r-- | usr/df.c | 26 | ||||
| -rw-r--r-- | usr/help.c | 25 | ||||
| -rw-r--r-- | usr/ls.c | 68 | ||||
| -rw-r--r-- | usr/mv.c | 51 | ||||
| -rw-r--r-- | usr/poweroff.c | 11 | ||||
| -rw-r--r-- | usr/sleep.c | 16 | ||||
| -rw-r--r-- | usr/tail.c | 75 | ||||
| -rw-r--r-- | usr/tee.c | 25 |
11 files changed, 355 insertions, 12 deletions
@@ -28,7 +28,8 @@ $(BUILD)/%.o: src/%.asm $(INCS) | $(BUILD) PROGS := chello echo true false uname pid cat wc head args \ ls save rm sh ps kill spin blk mkdir count ptest necho \ wget chat netd ping nslookup dhcp irc uptime ansi \ - grep more cp nc httpd ircd + grep more cp nc httpd ircd \ + df mv sleep tee tail help poweroff CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS)) $(BUILD)/programs.o: $(CBLOBS) @@ -17,9 +17,12 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* background jobs (`spin &`), `ps`, `kill` — round-robin cooperative scheduling across banked memory with an HRAM context switch. - **A shell** (written in C, running *on* the GB) with I/O redirection - (`>` `<`) and pipes (`|`), argument passing, and a cwd-aware prompt — with - `grep` to put on the other end of the pipe, and `more` to page anything - longer than the 18-row screen. + (`>` `<` `>>`) and pipes (`|`), argument passing, and a cwd-aware prompt — + with `grep` to put on the other end of the pipe, `tee` to fork it, and + `more` to page anything longer than the 18-row screen. +- **Shell scripts**: a shell fed from a file (`sh < script`) runs it with no + prompt and no echo, and init runs **`/rc`** at boot if the cart has one — + so a Game Boy can come up with its own startup script. - **Networking, for real**: the kernel owns SLIP over the link port, IPv4, ICMP, UDP and TCP; userland gets BSD-ish sockets via one syscall. On top: `dhcp` (boot-time lease), `ping`, `nslookup` (real DNS), `wget` (HTTP), @@ -42,9 +45,12 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* colors) + an **on-screen keyboard** on the window layer (SELECT toggles it). - **Persistent filesystem** on battery-backed cart RAM: inodes, nested directories, per-process cwd — survives power-off (files up to 3 KiB). -- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~35 - tools ship in `usr/` (`cat`, `wc`, `head`, `grep`, `cp`, `more`, `ls`, - `uptime`, …). Adding a program is one `.c` file + a table entry. + `ls -l` and `df` report sizes and free space; `poweroff` is the only safe + way to stop the machine (the battery save is written on a clean exit). +- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~45 + tools ship in `usr/` (`cat`, `wc`, `head`, `tail`, `grep`, `cp`, `mv`, + `tee`, `more`, `ls`, `df`, `sleep`, `uptime`, …) — `help` lists them all on + the Game Boy itself. Adding a program is one `.c` file + a table entry. ## Quick start diff --git a/src/programs.asm b/src/programs.asm index 0277dcc..11da2b1 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -157,6 +157,27 @@ ProgHttpd: SECTION "prog_ircd", ROMX[$4000], BANK[40] ProgIrcd: INCBIN "build/usr/ircd.bin" +SECTION "prog_df", ROMX[$4000], BANK[41] +ProgDf: + INCBIN "build/usr/df.bin" +SECTION "prog_mv", ROMX[$4000], BANK[42] +ProgMv: + INCBIN "build/usr/mv.bin" +SECTION "prog_sleep", ROMX[$4000], BANK[43] +ProgSleep: + INCBIN "build/usr/sleep.bin" +SECTION "prog_tee", ROMX[$4000], BANK[44] +ProgTee: + INCBIN "build/usr/tee.bin" +SECTION "prog_tail", ROMX[$4000], BANK[45] +ProgTail: + INCBIN "build/usr/tail.bin" +SECTION "prog_help", ROMX[$4000], BANK[46] +ProgHelp: + INCBIN "build/usr/help.bin" +SECTION "prog_poweroff", ROMX[$4000], BANK[47] +ProgPoweroff: + INCBIN "build/usr/poweroff.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and @@ -277,6 +298,20 @@ ProgramTable:: dw ProgHttpd db LOW(BANK(ProgIrcd)), HIGH(BANK(ProgIrcd)) dw ProgIrcd + db LOW(BANK(ProgDf)), HIGH(BANK(ProgDf)) + dw ProgDf + db LOW(BANK(ProgMv)), HIGH(BANK(ProgMv)) + dw ProgMv + db LOW(BANK(ProgSleep)), HIGH(BANK(ProgSleep)) + dw ProgSleep + db LOW(BANK(ProgTee)), HIGH(BANK(ProgTee)) + dw ProgTee + db LOW(BANK(ProgTail)), HIGH(BANK(ProgTail)) + dw ProgTail + db LOW(BANK(ProgHelp)), HIGH(BANK(ProgHelp)) + dw ProgHelp + db LOW(BANK(ProgPoweroff)), HIGH(BANK(ProgPoweroff)) + dw ProgPoweroff ProgramTableEnd:: ; ----------------------------------------------------------------------------- @@ -362,4 +397,18 @@ NameTable:: db PROG_HTTPD db "ircd", 0 db PROG_IRCD + db "df", 0 + db PROG_DF + db "mv", 0 + db PROG_MV + db "sleep", 0 + db PROG_SLEEP + db "tee", 0 + db PROG_TEE + db "tail", 0 + db PROG_TAIL + db "help", 0 + db PROG_HELP + db "poweroff", 0 + db PROG_POWEROFF db 0 diff --git a/usr/df.c b/usr/df.c new file mode 100644 index 0000000..ad1a000 --- /dev/null +++ b/usr/df.c @@ -0,0 +1,26 @@ +#include "stat.h" +/* df: how much of the cart's battery-backed disk is left. + * 256-byte blocks, 128 of them (32 KiB), 32 inodes - small enough that the + * numbers are worth watching. */ +void main(void) { + unsigned char fs[4]; + unsigned int used; + gfsstat(fs); + used = (unsigned int)(fs[0] - fs[1]); + puts("blocks "); + putu(used); + putc('/'); + putu(fs[0]); + puts(" used ("); + putu((unsigned int)used * 256 / 1024); + puts("K of "); + putu((unsigned int)fs[0] * 256 / 1024); + puts("K)"); + nl(); + puts("inodes "); + putu((unsigned int)(fs[2] - fs[3])); + putc('/'); + putu(fs[2]); + puts(" used"); + nl(); +} diff --git a/usr/help.c b/usr/help.c new file mode 100644 index 0000000..5a68d57 --- /dev/null +++ b/usr/help.c @@ -0,0 +1,25 @@ +#include "gbos.h" +/* help: list every command the shell knows. The kernel owns the name table + * (sys_lookup searches it), so walk program ids and ask for each one's name - + * the box had no way to discover what it can do. */ +void main(void) { + char name[16]; + unsigned char id, col = 0; + for (id = 0; id < 64; id++) { + name[0] = 0; + progname(id, name); + name[15] = 0; + if (!name[0]) continue; + if (lookup(name) == NOFD) continue; /* not reachable from the shell */ + puts(name); + col = (unsigned char)(col + 1); + if (col == 4) { + nl(); + col = 0; + } else { + unsigned char k = strlen(name); + while (k++ < 10) putc(' '); + } + } + if (col) nl(); +} @@ -1,10 +1,68 @@ -#include "gbos.h" -/* ls [dir]: list a directory (default: current). */ +#include "stat.h" +/* ls [-l] [dir]: list a directory (default: current). + * -l adds the type, link count and size - the terminal is 40 columns, so the + * long form is "d 1 256 name", not the BSD/GNU sprawl. */ + +static void pad(unsigned int n, unsigned char width) { + unsigned char d = 1; + unsigned int v = n; + while (v >= 10) { + v = v / 10; + d++; + } + while (d++ < width) putc(' '); + putu(n); +} + void main(void) { char *argv[4]; char name[16]; + char path[40]; + struct gstat st; unsigned char argc = argv_parse(argv, 4); - unsigned char i; - if (opendir(argc > 0 ? argv[0] : ".")) { puts("ls: no such directory"); nl(); return; } - for (i = 0; flist(i, name); i++) { name[15] = 0; puts(name); nl(); } + unsigned char lng = hasflag(argv, argc, 'l'); + unsigned char i, j, k; + char *dir = 0; + + for (i = 0; i < argc; i++) + if (argv[i][0] != '-') { + dir = argv[i]; + break; + } + if (opendir(dir ? dir : ".")) { + puts("ls: no such directory"); + nl(); + sexit(1); + } + + for (i = 0; flist(i, name); i++) { + name[15] = 0; + if (!lng) { + puts(name); + nl(); + continue; + } + /* stat wants a path, and flist only hands back the leaf name */ + j = 0; + if (dir) { + for (k = 0; dir[k] && j < 30; k++) path[j++] = dir[k]; + if (j && path[j - 1] != '/') path[j++] = '/'; + } + for (k = 0; name[k] && j < 38; k++) path[j++] = name[k]; + path[j] = 0; + if (!gstat(path, &st)) { + puts("? "); + puts(name); + nl(); + continue; + } + putc(st.type == ST_DIR ? 'd' : '-'); + putc(' '); + pad(st.nlink, 2); + putc(' '); + pad(st.size, 5); + putc(' '); + puts(name); + nl(); + } } diff --git a/usr/mv.c b/usr/mv.c new file mode 100644 index 0000000..76d9e62 --- /dev/null +++ b/usr/mv.c @@ -0,0 +1,51 @@ +#include "gbos.h" +/* mv SRC DST: there is no rename syscall, so this is cp + rm. The copy has to + * succeed before the source is removed - a half-copied DST plus a deleted SRC + * loses the file. */ + +static unsigned char streq(const char *a, const char *b) { + while (*a && *a == *b) { + a++; + b++; + } + return (unsigned char)(*a == *b); +} + +void main(void) { + char *argv[4]; + unsigned char argc = argv_parse(argv, 4); + unsigned char in, out; + int ch; + + if (argc < 2) { + puts("usage: mv SRC DST"); + nl(); + sexit(2); + } + if (streq(argv[0], argv[1])) sexit(0); /* nothing to do */ + + in = open(argv[0], O_READ); + if (in == EISDIR) { + puts("mv: SRC is a directory"); + nl(); + sexit(2); + } + if (in == NOFD) { + puts("mv: no such file: "); + puts(argv[0]); + nl(); + sexit(2); + } + out = open(argv[1], O_WRITE); + if (out > 3) { + puts("mv: cannot create "); + puts(argv[1]); + nl(); + close(in); + sexit(2); + } + while ((ch = fgetc(in)) >= 0) fputc(out, (char)ch); + close(in); + close(out); + fremove(argv[0]); /* only now is it safe */ +} diff --git a/usr/poweroff.c b/usr/poweroff.c new file mode 100644 index 0000000..d57dbc9 --- /dev/null +++ b/usr/poweroff.c @@ -0,0 +1,11 @@ +#include "gbos.h" +/* poweroff: shut the machine down cleanly. + * This is the ONLY safe way to stop a gbos with data on the disk: the cart's + * battery-backed RAM is only written out when the emulator exits properly, + * so pulling the plug (SIGKILL, closing the window) loses the whole + * filesystem - it comes back "formatted" on the next boot. */ +void main(void) { + puts("poweroff: syncing disk..."); + nl(); + poweroff(); +} diff --git a/usr/sleep.c b/usr/sleep.c new file mode 100644 index 0000000..3c229df --- /dev/null +++ b/usr/sleep.c @@ -0,0 +1,16 @@ +#include "gbos.h" +/* sleep N: wait N seconds (the kernel counts 1/64 s ticks, so N is capped at + * what fits: 3 seconds per gsleep call, looped). Mostly useful in scripts and + * to pace a background job. */ +void main(void) { + char *argv[4]; + unsigned char argc = argv_parse(argv, 4); + unsigned char n, i; + if (argc < 1) { + puts("usage: sleep SECONDS"); + nl(); + sexit(2); + } + n = atou(argv[0]); + for (i = 0; i < n; i++) gsleep(64); /* 64 ticks = one second */ +} diff --git a/usr/tail.c b/usr/tail.c new file mode 100644 index 0000000..1fd5304 --- /dev/null +++ b/usr/tail.c @@ -0,0 +1,75 @@ +#include "gbos.h" +/* tail [-n N] [N] [file]: last N lines (default 10) of a file or stdin. + * + * There is no seek syscall, so this cannot jump to the end: it streams the + * whole input through a ring of N lines and prints the ring at EOF. The ring + * is the memory budget - N lines of up to LINEW chars each - so N is capped. */ + +#define MAXN 12 /* 12 * 42 = 504 bytes of ring, fits a task */ +#define LINEW 42 + +void main(void) { + char *argv[8]; + char ring[MAXN][LINEW]; + unsigned char len[MAXN]; + unsigned char argc = argv_parse(argv, 8); + char *nv = optval(argv, argc, 'n'); + unsigned char n = 10, i, cur = 0, filled = 0, fd = NOFD, l = 0; + char *fname = 0; + int ch; + + if (nv) n = atou(nv); + for (i = 0; i < argc; i++) { + if (argv[i][0] == '-') continue; + if (nv == argv[i]) continue; + if (!nv && argv[i][0] >= '0' && argv[i][0] <= '9') { + n = atou(argv[i]); + continue; + } + fname = argv[i]; + break; + } + if (n == 0) return; + if (n > MAXN) n = MAXN; + + if (fname) { + fd = open(fname, O_READ); + if (fd == EISDIR) { + puts("tail: is a directory"); + nl(); + sexit(2); + } + if (fd == NOFD) { + puts("tail: no such file"); + nl(); + sexit(2); + } + } + for (i = 0; i < MAXN; i++) len[i] = 0; + + for (;;) { + if (fd == NOFD) { + char c = readc(); + ch = (c == EOF) ? -1 : c; + } else ch = fgetc(fd); + if (ch == '\r') continue; + if (ch >= 0 && ch != '\n') { + if (len[cur] < LINEW) ring[cur][len[cur]++] = (char)ch; + continue; + } + if (ch < 0 && len[cur] == 0) break; /* EOF, nothing pending */ + cur = (unsigned char)((cur + 1) % n); /* keep only the last n */ + if (filled < n) filled++; + len[cur] = 0; + if (ch < 0) break; + } + if (fd != NOFD) close(fd); + + /* oldest kept line first: cur points at the (empty) next slot */ + i = (unsigned char)((cur + n - filled) % n); + while (filled--) { + writes(ring[i], len[i]); + nl(); + i = (unsigned char)((i + 1) % n); + } +} diff --git a/usr/tee.c b/usr/tee.c new file mode 100644 index 0000000..dc086ce --- /dev/null +++ b/usr/tee.c @@ -0,0 +1,25 @@ +#include "gbos.h" +/* tee FILE: copy stdin to stdout AND to FILE. The missing half of a pipeline: + * `cat big | tee copy | wc -l` keeps the data and counts it in one pass. */ +void main(void) { + char *argv[4]; + unsigned char argc = argv_parse(argv, 4); + unsigned char fd = NOFD; + char c; + if (argc > 0) { + fd = open(argv[0], O_WRITE); + if (fd > 3) { + puts("tee: cannot create "); + puts(argv[0]); + nl(); + sexit(2); + } + } + for (;;) { + c = readc(); + if (c == EOF) break; + putc(c); + if (fd != NOFD) fputc(fd, c); + } + if (fd != NOFD) close(fd); +} |
