diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 13:14:07 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 13:14:07 +0200 |
| commit | 6eb14b64209b9d24554c587f610d7e300ef809ea (patch) | |
| tree | c308507a2140680eb82fdb8b360aa346e7c5c9fe /CONTEXT.md | |
| parent | Bump workspace to edition 2024 (diff) | |
| download | rs-dos-6eb14b64209b9d24554c587f610d7e300ef809ea.tar.gz rs-dos-6eb14b64209b9d24554c587f610d7e300ef809ea.zip | |
shell: keyword redirection + post-filter flags (ADR 0002)
Implement the shell experience redesign: one canonical grammar for every
command — 'cmd args [USE src] [TIL marker] [filters...] [output kw dst]'.
- Keyword redirection (uppercase-only, aliases valid, fully reserved):
WRITING/WRITE overwrite, TO append, USE/USING input source,
TIL/UNTIL heredoc/terminator (exact full-line marker). > >> < << are
removed; '>' is plain text.
- Post-filter stages applied left to right over a line stream, each
repeatable: -h N (head kills the upstream), -t N (tail holds a
rolling window, flushes at EOF/^C), -g PAT (smart-case literal
substring), -p (screenful pager, terminal only). A file sink
replaces the screen; -p is a no-op with one.
- C-style quoting: "..." groups and literalizes, \x escapes one char;
quoted/escaped text is never an operator, never split.
- Input consumers: cat/hexdump, path-less only (stdin until ^D,
USE f, TIL m heredoc, USE f TIL m truncation). Other commands
reject input keywords; paths + input keyword is a syntax error.
- ^D is EOF everywhere and never powers off; poweroff/exit are the
only shutdown verbs.
- help rewritten; test-disk.sh and sendkeys.py use the keyword syntax.
Verified in QEMU: 26 grammar checks on fresh and reused disks, disk
round trip, pager pacing, -h stopping /dev/mem at N lines, ^C flushing
a -t window. Two bugs found during verification and fixed:
1. WRITING over an existing file wrote after stale NUL padding —
fs::size() is stale after set_len(0); the sink writes at offset 0
after truncation.
2. USE f TIL m leaked the post-marker remainder — the final
partial-line emit ran after the marker break; guarded with a
stopped flag.
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 208 |
1 files changed, 204 insertions, 4 deletions
| @@ -168,8 +168,8 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 168 | (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices | 168 | (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices |
| 169 | regenerate on read, `/dev/mem` is raw physical RAM) | 169 | regenerate on read, `/dev/mem` is raw physical RAM) |
| 170 | ✅ **^C / ^D** — Ctrl+C cancels the line or aborts a running stream | 170 | ✅ **^C / ^D** — Ctrl+C cancels the line or aborts a running stream |
| 171 | (`cat`/`hexdump` check between chunks), Ctrl+D on an empty line | 171 | (`cat`/`hexdump` check between chunks); Ctrl+D is EOF (no poweroff — |
| 172 | powers off (EOF) | 172 | removed this session; see session notes below) |
| 173 | ✅ **tty + vt (terminal) — the 1:1 UNIX console split**: `tty` owns the | 173 | ✅ **tty + vt (terminal) — the 1:1 UNIX console split**: `tty` owns the |
| 174 | line discipline (canonical echo/buffer/^C/^D for the shell; raw mode | 174 | line discipline (canonical echo/buffer/^C/^D for the shell; raw mode |
| 175 | translates keys to the classic terminal byte protocol), `vt` owns the | 175 | translates keys to the classic terminal byte protocol), `vt` owns the |
| @@ -205,6 +205,32 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 205 | **Files survive reboot** — verified by poweroff → `mdir`/`mtype` on | 205 | **Files survive reboot** — verified by poweroff → `mdir`/`mtype` on |
| 206 | the image and by a fresh boot seeing the prior session's files. | 206 | the image and by a fresh boot seeing the prior session's files. |
| 207 | 207 | ||
| 208 | ✅ **Shell grammar (ADR 0002)** — one canonical grammar for every | ||
| 209 | command: `cmd args [USE src] [TIL marker] [filters…] [output kw dst]`. | ||
| 210 | Keyword redirection (`WRITING`/`WRITE` overwrite, `TO` append, | ||
| 211 | `USE`/`USING` input source, `TIL`/`UNTIL` heredoc/terminator; | ||
| 212 | uppercase-only, aliases valid, fully reserved), C-style quoting | ||
| 213 | (`"…"` groups, `\x` escapes — quoted text is never an operator), | ||
| 214 | post-filter stages applied left to right (`-h N` head kills the | ||
| 215 | upstream, `-t N` tail holds back and flushes at EOF/^C, `-g PAT` | ||
| 216 | smart-case literal substring, `-p` screenful pager; all repeatable), | ||
| 217 | `>`/`<`/`<<`/`>>` removed (`>` is plain text), file sinks replace the | ||
| 218 | screen, `cat`/`hexdump` consume input when path-less (stdin heredoc, | ||
| 219 | `cat USE a TO b` copy, `USE f TIL m` truncation). ^D is EOF | ||
| 220 | everywhere and never powers off. `help` rewritten; `test-disk.sh` | ||
| 221 | and `sendkeys.py` updated to the keyword syntax. | ||
| 222 | |||
| 223 | Verified in QEMU (26 grammar checks on a fresh *and* a reused disk, | ||
| 224 | disk round trip, pager pacing): head kills at N lines, ^C flushes a | ||
| 225 | `-t` window, smart-case grep, chained/repeated filters, `WRITING`+ | ||
| 226 | `TO`, `USE..TO` copy, `USE..TIL`, heredoc → file, quoting, `-p`. | ||
| 227 | Two bugs found and fixed during verification: (1) `WRITING` over an | ||
| 228 | existing file wrote after stale NUL padding — `fs::size()` returns | ||
| 229 | the node-cached size, which is stale after `set_len(0)`; the sink | ||
| 230 | now writes at literal offset 0 after truncation; (2) `USE f TIL m` | ||
| 231 | leaked everything after the marker — the final partial-line emit | ||
| 232 | ran even after the marker break; fixed with a `stopped` flag. | ||
| 233 | |||
| 208 | QEMU setup: `-drive id=disk,if=none,format=raw,file=disk.img` + | 234 | QEMU setup: `-drive id=disk,if=none,format=raw,file=disk.img` + |
| 209 | `-device ide-hd,drive=disk,bus=ide.0` puts the data disk on AHCI port | 235 | `-device ide-hd,drive=disk,bus=ide.0` puts the data disk on AHCI port |
| 210 | 0; the ESP gets `bus=ide.1`. (The implicit `-drive file=fat:rw:esp` | 236 | 0; the ESP gets `bus=ide.1`. (The implicit `-drive file=fat:rw:esp` |
| @@ -223,7 +249,7 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 223 | - `make test-disk` / `./scripts/test-disk.sh` — full automated round | 249 | - `make test-disk` / `./scripts/test-disk.sh` — full automated round |
| 224 | trip: build → deploy → ensure image → boot QEMU headless → drive | 250 | trip: build → deploy → ensure image → boot QEMU headless → drive |
| 225 | the shell via `sendkey` through the monitor (`ls /disk`, `mkdir`, | 251 | the shell via `sendkey` through the monitor (`ls /disk`, `mkdir`, |
| 226 | `echo >`, `cat`, `poweroff`) → fsck-clean check → `mtype` verifies | 252 | `echo WRITING`, `cat`, `poweroff`) → fsck-clean check → `mtype` verifies |
| 227 | the written file survived poweroff | 253 | the written file survived poweroff |
| 228 | - `./scripts/sendkeys.py` — types lines into the guest via the QEMU | 254 | - `./scripts/sendkeys.py` — types lines into the guest via the QEMU |
| 229 | monitor (`--sock`, US layout, named keys for punctuation; bare | 255 | monitor (`--sock`, US layout, named keys for punctuation; bare |
| @@ -269,7 +295,7 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 269 | 128 chars ≈ 16 KB, sequence numbers, wrap/drop accounting. | 295 | 128 chars ≈ 16 KB, sequence numbers, wrap/drop accounting. |
| 270 | - **`klog` command** = thin alias for `cat /dev/klog` plus filters | 296 | - **`klog` command** = thin alias for `cat /dev/klog` plus filters |
| 271 | `-h` (head) and `-t` (tail); no log levels, no timestamps, nothing | 297 | `-h` (head) and `-t` (tail); no log levels, no timestamps, nothing |
| 272 | fancier. `/dev/klog` is a char device so `klog > /disk/…/boot.log` | 298 | fancier. `/dev/klog` is a char device so `klog WRITING /disk/…/boot.log` |
| 273 | persists a boot log via the existing redirection. | 299 | persists a boot log via the existing redirection. |
| 274 | - **Disk recognition scope — keep simple for now**: no pre-EBS UEFI | 300 | - **Disk recognition scope — keep simple for now**: no pre-EBS UEFI |
| 275 | block-device census, no GPT/MBR partition parsing, no NVMe driver. | 301 | block-device census, no GPT/MBR partition parsing, no NVMe driver. |
| @@ -280,3 +306,177 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 280 | ## Target audience | 306 | ## Target audience |
| 281 | 307 | ||
| 282 | Learning exercise and toy. Not a Linux replacement. Not POSIX-compatible. | 308 | Learning exercise and toy. Not a Linux replacement. Not POSIX-compatible. |
| 309 | |||
| 310 | --- | ||
| 311 | |||
| 312 | ## Session notes — shell experience redesign (implemented) | ||
| 313 | |||
| 314 | Working from `shell_experience`. Plan as stated: | ||
| 315 | |||
| 316 | - Commands accept **post-filters** as trailing flags: `-h N` (head), | ||
| 317 | `-t N` (tail), `-g PAT` (grep). Flag placement (at the end of the | ||
| 318 | command) is respected. | ||
| 319 | - Redirection uses **keywords**, not `> << < >>`: | ||
| 320 | `WRITING`/`WRITE` → overwrite, `UNTIL`/`TIL` → heredoc, | ||
| 321 | `USE`/`USING` → input, `TO` → append. | ||
| 322 | - **No pipes.** `head`/`tail`/`grep` needs are covered by the flags; | ||
| 323 | a pager flag is a maybe. | ||
| 324 | |||
| 325 | Resolved this session: | ||
| 326 | |||
| 327 | - **Post-filter order = written order, left to right.** Each flag is a | ||
| 328 | filter stage applied to the command's output as a line stream: | ||
| 329 | `cat file -h 50 -t 10 -g pat` = "lines containing `pat` among the last | ||
| 330 | 10 lines of the first 50 lines". (The `shell_experience` prose for | ||
| 331 | that example was loose; the left-to-right rule is canonical.) | ||
| 332 | - **`-t N` holds back.** The tail stage buffers a rolling window of the | ||
| 333 | last N lines and emits nothing until its upstream ends — EOF, or the | ||
| 334 | user's ^C, which flushes the window to the screen. `cat /dev/mem -g | ||
| 335 | 100 -t 10` therefore shows nothing while it runs and prints the last 10 | ||
| 336 | matches when ^C ends it. | ||
| 337 | - **`-h N` truncates.** Once N lines have passed through, the head stage | ||
| 338 | kills the upstream command — `cat /dev/mem -h 5` peeks 5 lines and | ||
| 339 | stops instead of flooding. | ||
| 340 | - **`-g PAT` = smart-case literal substring.** Match is case-insensitive | ||
| 341 | unless PAT contains an uppercase letter, in which case it is | ||
| 342 | case-sensitive (editor-style smart case). No regex, no wildcards. | ||
| 343 | - **Post-filters repeat.** `-h`, `-t` and `-g` may each appear more than | ||
| 344 | once; every instance is a separate stage in the left-to-right chain | ||
| 345 | (so `-h 10 -h 5` = first 5, `-t 10 -t 5` = last 5, `-g foo -g bar` = | ||
| 346 | lines containing both). After `-g`, the next token is the pattern | ||
| 347 | verbatim (`-g -h` = pattern "-h"). | ||
| 348 | - **Redirection keywords pair strictly.** At most one input keyword and | ||
| 349 | one output keyword per command line (both may appear together); the | ||
| 350 | input keyword must precede the output keyword. `USING f TO g` is | ||
| 351 | accepted, `TO g USING f` is a syntax error, and so is any second | ||
| 352 | keyword of the same kind (`TO a WRITE b`). | ||
| 353 | - **Trailing-region grammar (canonical order):** | ||
| 354 | `cmd args [USE src] [TIL marker] [filters…] [output kw dst]` — | ||
| 355 | input keywords first, then filter flags, then the output keyword. | ||
| 356 | Rationale: "we feed the command input, parse it with filters, put | ||
| 357 | that into output". `USE` must precede `TIL`; filters come after the | ||
| 358 | input keywords and before the output keyword. | ||
| 359 | - **`TIL` is a terminator, not a source.** Unlike `USE` (which names an | ||
| 360 | input source), `TIL <marker>` ends the input at a marker, so it may | ||
| 361 | combine with `USE` — two "input" keywords are allowed, in the fixed | ||
| 362 | order `USE` then `TIL`. | ||
| 363 | - **`TIL` truncates the source at the marker.** The marker line is not | ||
| 364 | part of the input and everything from the marker on is discarded — | ||
| 365 | `USE f TIL m` reads only the text of `f` before `m`. Marker match is | ||
| 366 | an exact full-line match. | ||
| 367 | - **^D is EOF everywhere; ^D/^C no longer power off.** Inside a `TIL` | ||
| 368 | input block, ^D ends the input early exactly like a file's EOF. | ||
| 369 | Shutting down via ^D or ^C is removed: at the prompt, ^D on an empty | ||
| 370 | line is a no-op (fresh prompt); ^C keeps its line-cancel / | ||
| 371 | stream-abort role; `poweroff`/`exit` remain the only shutdown verbs. | ||
| 372 | - **Input consumers: `cat` and `hexdump` only, and only when path-less.** | ||
| 373 | With no path (or with an input keyword), they read the input stream | ||
| 374 | (`cat USE a TO b` copies a→b; `cat TIL END` is the classic heredoc). | ||
| 375 | All other commands reject input keywords with a syntax error — a | ||
| 376 | command declares whether it takes input. `USE`/`TIL` are general | ||
| 377 | building blocks; more consumers will come later. | ||
| 378 | - **Keywords: uppercase-only, aliases valid.** `WRITE`==`WRITING`, | ||
| 379 | `TIL`==`UNTIL`, `USE`==`USING`; lowercase forms are ordinary text, so | ||
| 380 | `echo use the force` can never redirect. Keywords are reserved | ||
| 381 | operators — a keyword is always an operator, never an argument | ||
| 382 | (collision escape: quoting, a future building block). | ||
| 383 | - **`-p` = pause flag (pager), more-style.** A stage that pauses at | ||
| 384 | every screenful of output and waits for a keypress; engages only when | ||
| 385 | output exceeds one screenful, no args, no scrollback/search. As a | ||
| 386 | stage it chains with the others — `-h N -p -g pat -p` lets the user | ||
| 387 | see intermediate steps of the filtering. | ||
| 388 | - **`> >> < <<` are gone.** Keywords are the *only* redirection syntax; | ||
| 389 | `>` is now ordinary text (`echo a > b` prints `a > b`). Collateral: | ||
| 390 | `scripts/test-disk.sh` and the `help` text must be rewritten to the | ||
| 391 | keyword syntax; the milestone bullets above describing `echo >`/`>>` | ||
| 392 | record the old behavior (superseded by these notes). | ||
| 393 | - **Quoting (C): `"..."` groups + literalizes; `\x` escapes one char.** | ||
| 394 | Quoted/escaped text is always literal — never an operator, never | ||
| 395 | split. So `echo foo "WRITING" bar` prints `foo WRITING bar`; to | ||
| 396 | print literal quote characters: `echo foo \"WRITING\" bar`. Also | ||
| 397 | enables spaces in filenames and multi-word `-g` patterns | ||
| 398 | (`-g "a b"`). Quote chars are delimiters, not content. | ||
| 399 | |||
| 400 | Assumed edge semantics (flag if wrong): | ||
| 401 | |||
| 402 | - Filtered output goes to both terminal (GOP) and serial, identically. | ||
| 403 | A file sink (WRITING/TO) replaces terminal output entirely — screen | ||
| 404 | is silent, as `echo >` behaved before. | ||
| 405 | - `-p` pauses terminal output only; with a file sink it is a no-op. | ||
| 406 | - `-h N` / `-t N` require N ≥ 1; otherwise syntax error. | ||
| 407 | - `cat p USE s` (path args *and* an input keyword) is a syntax error — | ||
| 408 | the input source is ambiguous. `cat USE s` and path-less `cat TIL m` | ||
| 409 | are the valid input forms. | ||
| 410 | - ADR 0002 (`docs/adr/0002-keyword-redirection-and-post-filters.md`) | ||
| 411 | captures the keyword-redirection + post-filter decision; the ADR is | ||
| 412 | the authoritative statement, these notes are the working detail. | ||
| 413 | |||
| 414 | Open questions (to resolve this session): | ||
| 415 | |||
| 416 | 1. ~~Order of application of `-h`/`-t`/`-g`~~ — **resolved**: left to | ||
| 417 | right. | ||
| 418 | 2. ~~`-h`/`-t` streaming semantics~~ — **resolved**: `-t` hold-back + | ||
| 419 | ^C flush; `-h` truncates the upstream. | ||
| 420 | 3. ~~`-h`/`-t` semantics on infinite streams (e.g. `cat /dev/mem`)~~ — | ||
| 421 | **resolved**: folded into 2. | ||
| 422 | 4. ~~`-g` pattern semantics (literal substring vs regex; case | ||
| 423 | sensitivity)~~ — **resolved**: smart-case literal substring; | ||
| 424 | repeatable stages. | ||
| 425 | 5. ~~Keyword details~~ — **resolved**: uppercase-only, both aliases | ||
| 426 | valid, keywords fully reserved; quoting (also resolved this | ||
| 427 | session) is the escape mechanism. | ||
| 428 | 6. ~~Pager flag: in or out~~ — **resolved**: `-p` pause flag, in. | ||
| 429 | 7. ~~Which commands consume input~~ — **resolved**: `cat` + `hexdump`, | ||
| 430 | path-less only; other commands reject input keywords. | ||
| 431 | 8. ~~Fate of the existing `>` / `>>` redirection~~ — **resolved**: full | ||
| 432 | removal; `>` is plain text; scripts/help to be rewritten. | ||
| 433 | |||
| 434 | ## Implementation (this session — done, verified in QEMU) | ||
| 435 | |||
| 436 | Everything above is now implemented in `shell/src/lib.rs`: | ||
| 437 | |||
| 438 | - **Tokenizer** — `"…"` groups text (delimiter, not content), `\x` | ||
| 439 | escapes one char, a token containing any quoted/escaped char is | ||
| 440 | flagged literal and can never be an operator or flag. | ||
| 441 | - **Parser** — canonical grammar for every command; keywords | ||
| 442 | uppercase-only with aliases, fully reserved (quoted forms are | ||
| 443 | literal data); strict ordering enforced (USE before TIL, input | ||
| 444 | before filters, output keyword last) with syntax errors otherwise; | ||
| 445 | `-h`/`-t` need N ≥ 1, `-g` takes the next token verbatim; unknown | ||
| 446 | flags before the trailing region are ordinary arguments. | ||
| 447 | - **Filter pipeline** — stages over a line stream, left to right, | ||
| 448 | each repeatable: `Head` kills the upstream (producers check the | ||
| 449 | `line()` return), `Tail` holds a rolling window and flushes through | ||
| 450 | the remaining stages at end-of-stream (EOF, ^C, or head kill), | ||
| 451 | `Grep` is smart-case literal substring, `Pause` is a terminal-only | ||
| 452 | screenful pager (^C aborts; dropped entirely with a file sink). | ||
| 453 | - **Sinks** — terminal (GOP + serial, identical) or file; a file sink | ||
| 454 | replaces the screen, opens lazily on first output, `WRITING` | ||
| 455 | truncates and writes from offset 0, `TO` appends at current size. | ||
| 456 | Diagnostics (errors, usage) always go to the terminal, never into | ||
| 457 | the pipeline or a file. | ||
| 458 | - **Input consumers** — `cat`/`hexdump`, path-less only: no keyword = | ||
| 459 | keyboard until ^D, `USE f` = file, `TIL m` = keyboard heredoc, | ||
| 460 | `USE f TIL m` = file truncated at the exact marker line (marker and | ||
| 461 | everything after discarded). Other commands reject input keywords. | ||
| 462 | - **^D** — EOF everywhere, at the prompt it is a no-op (fresh | ||
| 463 | prompt); ^C keeps line-cancel / stream-abort; `poweroff`/`exit` | ||
| 464 | are the only shutdown verbs. | ||
| 465 | - **Collateral** — `help` text rewritten; `scripts/test-disk.sh` and | ||
| 466 | the `sendkeys.py` docstring updated to keyword syntax. | ||
| 467 | |||
| 468 | Bugs found by QEMU verification and fixed: | ||
| 469 | |||
| 470 | 1. `WRITING` over an existing file left stale NUL padding — | ||
| 471 | `fs::size()` returns the node-cached size, stale after | ||
| 472 | `set_len(0)`; the overwrite path now writes at literal offset 0 | ||
| 473 | (the pre-redesign echo wrote at 0 too). | ||
| 474 | 2. `cat USE f TIL m` leaked everything after the marker — the | ||
| 475 | final-partial-line emit ran even after the marker break; fixed | ||
| 476 | with a `stopped` flag in `cat_file_until`/`hexdump_file_until`. | ||
| 477 | |||
| 478 | Verified: 26 grammar checks pass on a fresh disk *and* on a reused | ||
| 479 | one (rewriting existing files), `make test-disk` round trip is | ||
| 480 | fsck-clean, the `-p` pager pauses at screenfuls and resumes on | ||
| 481 | keypress, `-h` stops `/dev/mem` at exactly N lines, and ^C flushes a | ||
| 482 | `-t` window on an infinite stream. | ||
