aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-24 12:48:51 +0200
committeruser <user@clank>2026-09-24 12:48:51 +0200
commit65b53de14dc6918db39b6ee6dc032f805182daad (patch)
tree6af7b52262321a3cbe11e1d472ca2938df2fc37e
parentsh: run scripts, and >> redirection (diff)
downloadgbos-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--Makefile3
-rw-r--r--README.md18
-rw-r--r--src/programs.asm49
-rw-r--r--usr/df.c26
-rw-r--r--usr/help.c25
-rw-r--r--usr/ls.c68
-rw-r--r--usr/mv.c51
-rw-r--r--usr/poweroff.c11
-rw-r--r--usr/sleep.c16
-rw-r--r--usr/tail.c75
-rw-r--r--usr/tee.c25
11 files changed, 355 insertions, 12 deletions
diff --git a/Makefile b/Makefile
index 53c8f21..de7175b 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/README.md b/README.md
index c84eb43..8596fe2 100644
--- a/README.md
+++ b/README.md
@@ -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();
+}
diff --git a/usr/ls.c b/usr/ls.c
index 0e2e01f..5a26717 100644
--- a/usr/ls.c
+++ b/usr/ls.c
@@ -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);
+}