diff options
| author | user <user@clank> | 2026-09-24 22:45:04 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-24 22:45:04 +0200 |
| commit | 4cd933009dd6ce87fe710acfca6b1f786d51040c (patch) | |
| tree | 67e50c2ab347ddc2ac3e5b6a5cf5a4cfc44cc8d0 | |
| parent | fs: mount the builtins at /bin (diff) | |
| download | gbos-4cd933009dd6ce87fe710acfca6b1f786d51040c.tar.gz gbos-4cd933009dd6ce87fe710acfca6b1f786d51040c.tar.xz gbos-4cd933009dd6ce87fe710acfca6b1f786d51040c.zip | |
usr: touch, tr, rev, cut, uniq
/# ls -l /bin > b
/# cut -d \s -f 1 b | uniq -c
52 -
- touch FILE...: gbfs has no timestamps (an MBC5 cart has no RTC), so touch
does the half that means something here - make an empty file. O_APPEND is
exactly right: it creates, and it does not truncate what is already there.
- tr [-d] SET1 [SET2]: ranges (a-z), -d to delete, and a short SET2 padded
with its last character, like tr(1). ASCII only - a 128-entry map.
- rev, uniq [-c]: one line each, from a file or stdin like the other filters.
Two things the tests caught. A bare "-" is a SET, not a flag, so `tr . -`
has to skip flag-matching only when a letter follows the dash. And uniq
compared an empty "line" against the last run at EOF and flushed a phantom
blank one - a stray "1" in uniq -c output.
tr and cut also take \s \t \n \r escapes, because the shell has no quoting:
`-d ' '` arrives as three tokens, so a space or tab delimiter was simply
untypeable. Shell quoting would be the better fix; this makes the tools
usable today without touching the parser.
Banks 49-53; 10 program slots left in the 1 MB ROM.
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | include/gbos.inc | 5 | ||||
| -rw-r--r-- | src/programs.asm | 35 | ||||
| -rw-r--r-- | usr/cut.c | 89 | ||||
| -rw-r--r-- | usr/rev.c | 43 | ||||
| -rw-r--r-- | usr/touch.c | 26 | ||||
| -rw-r--r-- | usr/tr.c | 87 | ||||
| -rw-r--r-- | usr/uniq.c | 84 |
9 files changed, 374 insertions, 3 deletions
@@ -29,7 +29,8 @@ 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 \ - df mv sleep tee tail help poweroff xxd + df mv sleep tee tail help poweroff xxd \ + touch tr rev cut uniq CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS)) $(BUILD)/programs.o: $(CBLOBS) @@ -51,9 +51,10 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* show up as one anyway — `ls /bin` enumerates the kernel's own command table, `cd /bin` works, and `xxd -l 16 /bin/uname` dumps the program's ROM bank. Read-only, since it is ROM. -- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~45 +- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~50 tools ship in `usr/` (`cat`, `wc`, `head`, `tail`, `grep`, `cp`, `mv`, - `tee`, `more`, `ls`, `df`, `xxd`, `sleep`, `uptime`, …) — `help` lists them all on + `tee`, `tr`, `cut`, `uniq`, `rev`, `more`, `ls`, `df`, `xxd`, + `touch`, `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/include/gbos.inc b/include/gbos.inc index 32418b6..a51dd31 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -338,5 +338,10 @@ DEF PROG_TAIL EQU 43 DEF PROG_HELP EQU 44 DEF PROG_POWEROFF EQU 45 DEF PROG_XXD EQU 46 +DEF PROG_TOUCH EQU 47 +DEF PROG_TR EQU 48 +DEF PROG_REV EQU 49 +DEF PROG_CUT EQU 50 +DEF PROG_UNIQ EQU 51 ENDC diff --git a/src/programs.asm b/src/programs.asm index a602350..e9e1be9 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -181,6 +181,21 @@ ProgPoweroff: SECTION "prog_xxd", ROMX[$4000], BANK[48] ProgXxd: INCBIN "build/usr/xxd.bin" +SECTION "prog_touch", ROMX[$4000], BANK[49] +ProgTouch: + INCBIN "build/usr/touch.bin" +SECTION "prog_tr", ROMX[$4000], BANK[50] +ProgTr: + INCBIN "build/usr/tr.bin" +SECTION "prog_rev", ROMX[$4000], BANK[51] +ProgRev: + INCBIN "build/usr/rev.bin" +SECTION "prog_cut", ROMX[$4000], BANK[52] +ProgCut: + INCBIN "build/usr/cut.bin" +SECTION "prog_uniq", ROMX[$4000], BANK[53] +ProgUniq: + INCBIN "build/usr/uniq.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and @@ -317,6 +332,16 @@ ProgramTable:: dw ProgPoweroff db LOW(BANK(ProgXxd)), HIGH(BANK(ProgXxd)) dw ProgXxd + db LOW(BANK(ProgTouch)), HIGH(BANK(ProgTouch)) + dw ProgTouch + db LOW(BANK(ProgTr)), HIGH(BANK(ProgTr)) + dw ProgTr + db LOW(BANK(ProgRev)), HIGH(BANK(ProgRev)) + dw ProgRev + db LOW(BANK(ProgCut)), HIGH(BANK(ProgCut)) + dw ProgCut + db LOW(BANK(ProgUniq)), HIGH(BANK(ProgUniq)) + dw ProgUniq ProgramTableEnd:: ; ----------------------------------------------------------------------------- @@ -418,4 +443,14 @@ NameTable:: db PROG_POWEROFF db "xxd", 0 db PROG_XXD + db "touch", 0 + db PROG_TOUCH + db "tr", 0 + db PROG_TR + db "rev", 0 + db PROG_REV + db "cut", 0 + db PROG_CUT + db "uniq", 0 + db PROG_UNIQ db 0 diff --git a/usr/cut.c b/usr/cut.c new file mode 100644 index 0000000..18de461 --- /dev/null +++ b/usr/cut.c @@ -0,0 +1,89 @@ +#include "gbos.h" +/* cut -f N [-d C] [file]: print field N of each line. + * + * The delimiter defaults to TAB, like cut(1); -d takes the first character of + * its argument, so `-d :` works - and since the shell has no quoting, `-d \\s` + * is how you say space (\\t tab, \\n newline). A line with no delimiter is passed through + * whole (what GNU cut does without -s), which is usually what you want when + * filtering mixed output on a 40-column screen. */ + +#define MAXLINE 80 + +/* The shell has no quoting, so a space or a tab cannot be typed as an + argument: "-d ' '" arrives as three tokens. Escapes are how you name them + here - \s space, \t tab, \n newline, \\ backslash. */ +static char unesc1(const char *s, unsigned char *used) { + *used = 1; + if (s[0] != '\\' || !s[1]) return s[0]; + *used = 2; + switch (s[1]) { + case 's': return ' '; + case 't': return '\t'; + case 'n': return '\n'; + case 'r': return '\r'; + default: return s[1]; + } +} + +void main(void) { + char *argv[8]; + char line[MAXLINE]; + unsigned char argc = argv_parse(argv, 8); + char *fv = optval(argv, argc, 'f'); + char *dv = optval(argv, argc, 'd'); + unsigned char dused; + char delim = dv ? unesc1(dv, &dused) : '\t'; + unsigned char want = fv ? atou(fv) : 1; + unsigned char len = 0, fd = NOFD, i, field, start, end; + char *fname = 0; + int ch; + + if (want == 0) want = 1; + for (i = 0; i < argc; i++) { + if (argv[i][0] == '-') continue; + if (fv == argv[i] || dv == argv[i]) continue; + fname = argv[i]; + break; + } + if (fname) { + fd = open(fname, O_READ); + if (fd == EISDIR) { + puts("cut: is a directory"); + nl(); + sexit(2); + } + if (fd == NOFD) { + puts("cut: no such file"); + nl(); + sexit(2); + } + } + 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 < MAXLINE) line[len++] = (char)ch; + continue; + } + if (ch < 0 && len == 0) break; + /* walk to the wanted field */ + field = 1; + start = 0; + for (i = 0; i <= len; i++) { + if (i == len || line[i] == delim) { + if (field == want) break; + field++; + start = (unsigned char)(i + 1); + } + } + end = i; + if (field == want && start <= len) writes(line + start, (unsigned char)(end - start)); + nl(); + len = 0; + if (ch < 0) break; + } + if (fd != NOFD) close(fd); +} diff --git a/usr/rev.c b/usr/rev.c new file mode 100644 index 0000000..2932190 --- /dev/null +++ b/usr/rev.c @@ -0,0 +1,43 @@ +#include "gbos.h" +/* rev [file]: reverse the characters of each line, from a file or stdin. */ + +#define MAXLINE 80 + +void main(void) { + char *argv[4]; + char line[MAXLINE]; + unsigned char argc = argv_parse(argv, 4); + unsigned char len = 0, fd = NOFD, i; + int ch; + + if (argc > 0 && argv[0][0] != '-') { + fd = open(argv[0], O_READ); + if (fd == EISDIR) { + puts("rev: is a directory"); + nl(); + sexit(2); + } + if (fd == NOFD) { + puts("rev: no such file"); + nl(); + sexit(2); + } + } + 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 < MAXLINE) line[len++] = (char)ch; + continue; + } + if (ch < 0 && len == 0) break; + for (i = len; i--;) putc(line[i]); + nl(); + len = 0; + if (ch < 0) break; + } + if (fd != NOFD) close(fd); +} diff --git a/usr/touch.c b/usr/touch.c new file mode 100644 index 0000000..4f5ba89 --- /dev/null +++ b/usr/touch.c @@ -0,0 +1,26 @@ +#include "gbos.h" +/* touch FILE...: create FILE if it does not exist, leave it alone if it does. + * + * There are no timestamps in gbfs (no RTC on an MBC5 cart), so touch only does + * the half that means anything here: make an empty file. O_APPEND is exactly + * right for it - it creates, and it does NOT truncate what is already there. */ +void main(void) { + char *argv[8]; + unsigned char argc = argv_parse(argv, 8); + unsigned char i, fd; + if (argc < 1) { + puts("usage: touch FILE..."); + nl(); + sexit(2); + } + for (i = 0; i < argc; i++) { + fd = open(argv[i], O_APPEND); + if (fd > 3) { + puts("touch: cannot create "); + puts(argv[i]); + nl(); + continue; + } + close(fd); + } +} diff --git a/usr/tr.c b/usr/tr.c new file mode 100644 index 0000000..a98cf1d --- /dev/null +++ b/usr/tr.c @@ -0,0 +1,87 @@ +#include "gbos.h" +/* tr [-d] SET1 [SET2]: translate or delete characters, stdin to stdout. + * + * ls | tr a-z A-Z ranges work + * cat f | tr -d aeiou -d deletes instead of mapping + * echo a b | tr \\s \\n \\s \\t \\n name characters the shell cannot quote + * + * ASCII only (a 128-entry map on the stack), and like tr(1) a short SET2 is + * padded with its last character. */ + +#define SETMAX 64 + +/* The shell has no quoting, so a space or a tab cannot be typed as an + argument: "-d ' '" arrives as three tokens. Escapes are how you name them + here - \s space, \t tab, \n newline, \\ backslash. */ +static char unesc1(const char *s, unsigned char *used) { + *used = 1; + if (s[0] != '\\' || !s[1]) return s[0]; + *used = 2; + switch (s[1]) { + case 's': return ' '; + case 't': return '\t'; + case 'n': return '\n'; + case 'r': return '\r'; + default: return s[1]; + } +} + +/* expand "a-z0-9_" into the characters it names; returns how many */ +static unsigned char expand(const char *s, char *out, unsigned char max) { + unsigned char n = 0, i = 0; + char a, b; + while (s[i] && n < max) { + unsigned char ua, ub; + a = unesc1(s + i, &ua); + if (s[i + ua] == '-' && s[i + ua + 1]) { /* a range */ + b = unesc1(s + i + ua + 1, &ub); + while (a <= b && n < max) out[n++] = a++; + i += ua + 1 + ub; + } else { + out[n++] = a; + i += ua; + } + } + return n; +} + +void main(void) { + char *argv[8]; + char s1[SETMAX], s2[SETMAX]; + unsigned char map[128]; + unsigned char argc = argv_parse(argv, 8); + unsigned char del = hasflag(argv, argc, 'd'); + unsigned char i, n1 = 0, n2 = 0, k; + char *set1 = 0, *set2 = 0; + char c; + + for (i = 0; i < argc; i++) { + /* a bare "-" is a SET, not a flag: `tr . -` maps dots to dashes */ + if (argv[i][0] == '-' && argv[i][1]) continue; + if (!set1) set1 = argv[i]; + else if (!set2) set2 = argv[i]; + } + if (!set1 || (!del && !set2)) { + puts("usage: tr [-d] SET1 [SET2]"); + nl(); + sexit(2); + } + n1 = expand(set1, s1, SETMAX); + if (set2) n2 = expand(set2, s2, SETMAX); + + for (i = 0; i < 128; i++) map[i] = i; /* identity, then patch */ + for (k = 0; k < n1; k++) { + unsigned char from = (unsigned char)s1[k] & 127; + if (del) map[from] = 0; /* 0 = drop it */ + else map[from] = (unsigned char)s2[k < n2 ? k : n2 - 1]; + } + + for (;;) { + c = readc(); + if (c == EOF) break; + if ((unsigned char)c < 128) { + unsigned char o = map[(unsigned char)c]; + if (o) putc((char)o); + } else putc(c); + } +} diff --git a/usr/uniq.c b/usr/uniq.c new file mode 100644 index 0000000..2c6ebdc --- /dev/null +++ b/usr/uniq.c @@ -0,0 +1,84 @@ +#include "gbos.h" +/* uniq [-c] [file]: collapse runs of identical ADJACENT lines (like uniq(1) - + * it does not sort). -c prefixes each line with how many times it repeated. */ + +#define MAXLINE 80 + +static unsigned char lines_eq(const char *a, unsigned char la, const char *b, unsigned char lb) { + unsigned char i; + if (la != lb) return 0; + for (i = 0; i < la; i++) + if (a[i] != b[i]) return 0; + return 1; +} + +void main(void) { + char *argv[4]; + char cur[MAXLINE], prev[MAXLINE]; + unsigned char argc = argv_parse(argv, 4); + unsigned char cnt = hasflag(argv, argc, 'c'); + unsigned char clen = 0, plen = 0, have = 0, fd = NOFD, i; + unsigned int reps = 0; + char *fname = 0; + int ch; + + for (i = 0; i < argc; i++) + if (argv[i][0] != '-') { + fname = argv[i]; + break; + } + if (fname) { + fd = open(fname, O_READ); + if (fd == EISDIR) { + puts("uniq: is a directory"); + nl(); + sexit(2); + } + if (fd == NOFD) { + puts("uniq: no such file"); + nl(); + sexit(2); + } + } + 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 (clen < MAXLINE) cur[clen++] = (char)ch; + continue; + } + /* EOF with nothing buffered: stop. Falling through would compare an + empty "line" against the last run and flush a phantom blank one - + which showed up as a stray "1" in uniq -c. */ + if (ch < 0 && clen == 0) break; + if (have && lines_eq(cur, clen, prev, plen)) reps++; + else { + if (have) { /* flush the run that just ended */ + if (cnt) { + putu(reps); + putc(' '); + } + writes(prev, plen); + nl(); + } + for (i = 0; i < clen; i++) prev[i] = cur[i]; + plen = clen; + have = 1; + reps = 1; + } + clen = 0; + if (ch < 0) break; + } + if (have) { + if (cnt) { + putu(reps); + putc(' '); + } + writes(prev, plen); + nl(); + } + if (fd != NOFD) close(fd); +} |
