aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-24 13:56:42 +0200
committeruser <user@clank>2026-09-24 13:56:42 +0200
commit0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53 (patch)
tree2d50213ab441f2543ba129241ceda29c5d9b496d
parentircd: enforce unique nicknames (433), announce renames, fold case (diff)
downloadgbos-0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53.tar.gz
gbos-0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53.tar.xz
gbos-0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53.zip
usr: xxd - hex dump a file or stdin
Eight bytes per line rather than the usual sixteen: offset + 8 hex pairs + the ASCII column is 38 of the terminal's 40 columns, where 16 would wrap every line and make the dump unreadable. -c overrides it. No -s (skip): there is no seek syscall, so it streams from the start - the same reason tail(1) has to keep a ring. /# xxd h 0000: 48 65 6c 6c 6f 20 67 62 Hello gb 0008: 6f 73 0d 0a os.. blk(1) could already peek at raw disk blocks; nothing could look inside a file. Verified against known bytes, from stdin, with -c, through a pipe, and on a missing file.
-rw-r--r--Makefile2
-rw-r--r--README.md2
-rw-r--r--include/gbos.inc1
-rw-r--r--src/programs.asm7
-rw-r--r--usr/xxd.c82
5 files changed, 92 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index de7175b..b3f1b0e 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ 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
+ df mv sleep tee tail help poweroff xxd
CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS))
$(BUILD)/programs.o: $(CBLOBS)
diff --git a/README.md b/README.md
index 8596fe2..0dffb18 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).*
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
+ `tee`, `more`, `ls`, `df`, `xxd`, `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 76981da..1af3fc8 100644
--- a/include/gbos.inc
+++ b/include/gbos.inc
@@ -330,5 +330,6 @@ DEF PROG_TEE EQU 42
DEF PROG_TAIL EQU 43
DEF PROG_HELP EQU 44
DEF PROG_POWEROFF EQU 45
+DEF PROG_XXD EQU 46
ENDC
diff --git a/src/programs.asm b/src/programs.asm
index 11da2b1..a602350 100644
--- a/src/programs.asm
+++ b/src/programs.asm
@@ -178,6 +178,9 @@ ProgHelp:
SECTION "prog_poweroff", ROMX[$4000], BANK[47]
ProgPoweroff:
INCBIN "build/usr/poweroff.bin"
+SECTION "prog_xxd", ROMX[$4000], BANK[48]
+ProgXxd:
+ INCBIN "build/usr/xxd.bin"
; -----------------------------------------------------------------------------
; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and
@@ -312,6 +315,8 @@ ProgramTable::
dw ProgHelp
db LOW(BANK(ProgPoweroff)), HIGH(BANK(ProgPoweroff))
dw ProgPoweroff
+ db LOW(BANK(ProgXxd)), HIGH(BANK(ProgXxd))
+ dw ProgXxd
ProgramTableEnd::
; -----------------------------------------------------------------------------
@@ -411,4 +416,6 @@ NameTable::
db PROG_HELP
db "poweroff", 0
db PROG_POWEROFF
+ db "xxd", 0
+ db PROG_XXD
db 0
diff --git a/usr/xxd.c b/usr/xxd.c
new file mode 100644
index 0000000..8e255ae
--- /dev/null
+++ b/usr/xxd.c
@@ -0,0 +1,82 @@
+#include "gbos.h"
+/* xxd [-c N] [file]: hex dump a file, or stdin.
+ *
+ * Eight bytes per line, not the usual sixteen: offset + 8 hex pairs + the
+ * ASCII column is 38 of the terminal's 40 columns, where 16 would wrap every
+ * line and make the dump unreadable. -c overrides it anyway.
+ *
+ * There is no seek syscall, so this streams from the start - which is also
+ * why there is no -s (skip). */
+
+#define MAXC 16
+
+static char hexd(unsigned char v) { return (char)(v < 10 ? '0' + v : 'a' + (v - 10)); }
+static void hex2(unsigned char v) {
+ putc(hexd((unsigned char)(v >> 4)));
+ putc(hexd((unsigned char)(v & 15)));
+}
+static void hex4(unsigned int v) {
+ hex2((unsigned char)(v >> 8));
+ hex2((unsigned char)v);
+}
+
+void main(void) {
+ char *argv[8];
+ unsigned char buf[MAXC];
+ unsigned char argc = argv_parse(argv, 8);
+ char *cv = optval(argv, argc, 'c');
+ unsigned char cols = 8, fd = NOFD, n = 0, i;
+ unsigned int off = 0;
+ char *fname = 0;
+ int ch;
+
+ if (cv) {
+ cols = atou(cv);
+ if (cols == 0 || cols > MAXC) cols = 8;
+ }
+ for (i = 0; i < argc; i++) {
+ if (argv[i][0] == '-') continue;
+ if (cv == argv[i]) continue;
+ fname = argv[i];
+ break;
+ }
+ if (fname) {
+ fd = open(fname, O_READ);
+ if (fd == EISDIR) {
+ puts("xxd: is a directory");
+ nl();
+ sexit(2);
+ }
+ if (fd == NOFD) {
+ puts("xxd: no such file");
+ nl();
+ sexit(2);
+ }
+ }
+
+ for (;;) {
+ if (fd == NOFD) {
+ char c = readc();
+ ch = (c == EOF) ? -1 : c;
+ } else ch = fgetc(fd);
+ if (ch >= 0) buf[n++] = (unsigned char)ch;
+ if (n == cols || (ch < 0 && n)) { /* full line, or the last one */
+ hex4(off);
+ puts(": ");
+ for (i = 0; i < cols; i++) {
+ if (i < n) hex2(buf[i]);
+ else {
+ putc(' ');
+ putc(' ');
+ } /* pad so ASCII stays aligned */
+ putc(' ');
+ }
+ for (i = 0; i < n; i++) putc((buf[i] >= 32 && buf[i] < 127) ? (char)buf[i] : '.');
+ nl();
+ off += n;
+ n = 0;
+ }
+ if (ch < 0) break;
+ }
+ if (fd != NOFD) close(fd);
+}