diff options
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 208 | ||||
| -rw-r--r-- | docs/adr/0002-keyword-redirection-and-post-filters.md | 76 | ||||
| -rwxr-xr-x | scripts/sendkeys.py | 2 | ||||
| -rwxr-xr-x | scripts/test-disk.sh | 4 | ||||
| -rw-r--r-- | shell/src/lib.rs | 1458 |
5 files changed, 1416 insertions, 332 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. | ||
diff --git a/docs/adr/0002-keyword-redirection-and-post-filters.md b/docs/adr/0002-keyword-redirection-and-post-filters.md new file mode 100644 index 0000000..1584077 --- /dev/null +++ b/docs/adr/0002-keyword-redirection-and-post-filters.md | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | # ADR 0002 — Keyword Redirection and Post-Filter Flags Instead of Pipes | ||
| 2 | |||
| 3 | ## Status | ||
| 4 | |||
| 5 | Accepted. | ||
| 6 | |||
| 7 | ## Context | ||
| 8 | |||
| 9 | The shell (a freestanding, no_std kernel component) needs the data-flow | ||
| 10 | features every shell has: redirecting command output to files, feeding | ||
| 11 | input from files or typed blocks, and trimming output to `head`/`tail`/ | ||
| 12 | `grep`/pager views. The obvious model — Unix pipes plus `> < << >>` | ||
| 13 | operators — was considered and rejected. The project is explicitly | ||
| 14 | not POSIX-compatible, is TempleOS-inspired, and values minimal machinery | ||
| 15 | and natural-language-flavoured input over familiarity. The old shell | ||
| 16 | had only an ad-hoc `>`/`>>` parse baked into `cmd_echo`, no head/tail/ | ||
| 17 | grep at all, and ^D at the prompt powered the machine off. | ||
| 18 | |||
| 19 | ## Decision | ||
| 20 | |||
| 21 | One canonical command-line grammar, applied uniformly to every command: | ||
| 22 | |||
| 23 | ``` | ||
| 24 | cmd args [USE src] [TIL marker] [filters…] [output kw dst] | ||
| 25 | ``` | ||
| 26 | |||
| 27 | - **Redirection uses reserved uppercase keywords**, never symbols: | ||
| 28 | `WRITING`/`WRITE` overwrite, `TO` appends, `USE`/`USING` names the | ||
| 29 | input source, `TIL`/`UNTIL` truncates the input at an exact full-line | ||
| 30 | marker (marker and everything after it is discarded). At most one | ||
| 31 | input keyword (USE and TIL may pair, USE first) and one output | ||
| 32 | keyword; input keywords precede the filters, the output keyword | ||
| 33 | comes last. | ||
| 34 | - **Filters are trailing flags** applied left to right as stages over a | ||
| 35 | line stream: `-h N` (emit first N lines, then kill the upstream | ||
| 36 | command), `-t N` (hold back a rolling window of the last N lines; | ||
| 37 | emit only when the stream ends — EOF or ^C, which flushes), | ||
| 38 | `-g PAT` (smart-case literal substring match: case-insensitive unless | ||
| 39 | PAT contains an uppercase letter), `-p` (pause at each screenful, | ||
| 40 | `more`-style, only when output exceeds one screenful). Every flag | ||
| 41 | may repeat; each instance is another stage. After `-g`, the next | ||
| 42 | token is the pattern verbatim. | ||
| 43 | - **There are no pipes.** The four built-in filters cover the cases | ||
| 44 | pipes are usually built for; arbitrary command composition is | ||
| 45 | deliberately out of scope. | ||
| 46 | - **`> >> < <<` are removed** — `>` is now ordinary text. The shell | ||
| 47 | prints to both GOP and serial; a file sink replaces terminal output | ||
| 48 | entirely. | ||
| 49 | - **^D is EOF everywhere and no longer powers off** (^C keeps its | ||
| 50 | line-cancel / stream-abort role; `poweroff`/`exit` are the only | ||
| 51 | shutdown verbs). | ||
| 52 | - **Quoting (`"..."` groups and literalizes, `\x` escapes one char)** | ||
| 53 | is the escape hatch: quoted/escaped text is never an operator, which | ||
| 54 | makes keywords expressible as data and enables spaces in filenames. | ||
| 55 | |||
| 56 | ## Consequences | ||
| 57 | |||
| 58 | - **Positive:** One grammar, one parser — the per-command redirection | ||
| 59 | hacks (`cmd_echo`'s `>` scan) disappear. Filters compose uniformly | ||
| 60 | with any command and with each other, including intermediate views | ||
| 61 | (`-h N -p -g pat -p` shows each stage's result). No pipe machinery, | ||
| 62 | no process/stream abstraction to build. Lowercase text can never | ||
| 63 | collide with operators, so ordinary words stay ordinary. | ||
| 64 | - **Positive:** `tail` on an infinite stream (`cat /dev/mem -g 100 | ||
| 65 | -t 10`) is expressible without a pipe; ^C flushes the held-back | ||
| 66 | window instead of losing it. | ||
| 67 | - **Negative:** Breaks existing muscle memory and tooling: POSIX users | ||
| 68 | must learn the keywords; `scripts/test-disk.sh` and the `help` text | ||
| 69 | must be rewritten from `echo >` to the keyword syntax. | ||
| 70 | - **Negative:** Keywords are reserved, so printing text like `WRITING` | ||
| 71 | requires quoting; unknown commands cannot be filtered with flags | ||
| 72 | beyond the four built-ins; there is no way to compose arbitrary | ||
| 73 | commands (accepted — the shell has no external commands today). | ||
| 74 | - **Negative:** `-t` on a stream that never ends shows nothing until | ||
| 75 | the user ^C's — an intentional trade-off for simplicity (no pager | ||
| 76 | machinery, no infinite buffering beyond the window). | ||
diff --git a/scripts/sendkeys.py b/scripts/sendkeys.py index 5d1692c..1c97b33 100755 --- a/scripts/sendkeys.py +++ b/scripts/sendkeys.py | |||
| @@ -6,7 +6,7 @@ Each positional argument is one line: it is typed character by character | |||
| 6 | test script without a display. | 6 | test script without a display. |
| 7 | 7 | ||
| 8 | Usage: | 8 | Usage: |
| 9 | sendkeys.py "ls /disk" "echo hi > /disk/a.txt" "poweroff" | 9 | sendkeys.py "ls /disk" "echo hi WRITING /disk/a.txt" "poweroff" |
| 10 | sendkeys.py --sock /tmp/qmon.sock "date" | 10 | sendkeys.py --sock /tmp/qmon.sock "date" |
| 11 | 11 | ||
| 12 | Environment: QMON_SOCK (default: /tmp/qmon.sock) | 12 | Environment: QMON_SOCK (default: /tmp/qmon.sock) |
diff --git a/scripts/test-disk.sh b/scripts/test-disk.sh index f7337ef..11a9b05 100755 --- a/scripts/test-disk.sh +++ b/scripts/test-disk.sh | |||
| @@ -99,14 +99,14 @@ VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\ | |||
| 99 | [ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)" | 99 | [ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)" |
| 100 | echo " mounted volume UID: $VOL_UID" | 100 | echo " mounted volume UID: $VOL_UID" |
| 101 | 101 | ||
| 102 | echo "==> 4/6 drive the shell (ls / mkdir / echo > / cat / poweroff)" | 102 | echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)" |
| 103 | # The filename is deliberately >13 chars so the FAT LFN path gets | 103 | # The filename is deliberately >13 chars so the FAT LFN path gets |
| 104 | # multi-entry entries (they're stored last-first on disk). | 104 | # multi-entry entries (they're stored last-first on disk). |
| 105 | TESTFILE="disk-test-$STAMP.txt" | 105 | TESTFILE="disk-test-$STAMP.txt" |
| 106 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | 106 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ |
| 107 | "ls /disk" \ | 107 | "ls /disk" \ |
| 108 | "mkdir /disk/$VOL_UID/sub" \ | 108 | "mkdir /disk/$VOL_UID/sub" \ |
| 109 | "echo disk-test-$STAMP > /disk/$VOL_UID/sub/$TESTFILE" \ | 109 | "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \ |
| 110 | "cat /disk/$VOL_UID/sub/$TESTFILE" \ | 110 | "cat /disk/$VOL_UID/sub/$TESTFILE" \ |
| 111 | "poweroff" | 111 | "poweroff" |
| 112 | 112 | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 9644f01..bc43faf 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -1,10 +1,32 @@ | |||
| 1 | #![no_std] | 1 | #![no_std] |
| 2 | 2 | ||
| 3 | extern crate alloc; | 3 | //! Command shell — reads from PS/2 keyboard, writes to GOP + serial. |
| 4 | //! | ||
| 5 | //! One canonical grammar for every command (ADR 0002): | ||
| 6 | //! | ||
| 7 | //! ```text | ||
| 8 | //! cmd args [USE src] [TIL marker] [filters…] [output kw dst] | ||
| 9 | //! ``` | ||
| 10 | //! | ||
| 11 | //! - **Keywords** (uppercase-only, reserved operators): `WRITING`/`WRITE` | ||
| 12 | //! overwrite a file, `TO` appends, `USE`/`USING` names an input source, | ||
| 13 | //! `TIL`/`UNTIL` ends input at an exact full-line marker. | ||
| 14 | //! - **Filters** are trailing flags applied left to right as stages over a | ||
| 15 | //! line stream: `-h N` (first N lines, then kill the upstream), | ||
| 16 | //! `-t N` (hold back a rolling window of the last N lines, emit only at | ||
| 17 | //! end of stream), `-g PAT` (smart-case literal substring), `-p` (pause | ||
| 18 | //! at each screenful). Each may repeat. | ||
| 19 | //! - **Quoting** (`"..."` groups + literalizes, `\x` escapes one char) | ||
| 20 | //! makes quoted/escaped text literal — never an operator, never split. | ||
| 21 | //! - `>` and friends are gone; `>` is ordinary text. | ||
| 22 | //! - ^D is EOF everywhere and never powers off; `poweroff`/`exit` are the | ||
| 23 | //! only shutdown verbs. | ||
| 4 | 24 | ||
| 5 | use alloc::string::ToString; | 25 | extern crate alloc; |
| 6 | 26 | ||
| 7 | /// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. | 27 | use alloc::collections::VecDeque; |
| 28 | use alloc::string::{String, ToString}; | ||
| 29 | use alloc::vec::Vec; | ||
| 8 | use rsnix::tty::{self, LineInput}; | 30 | use rsnix::tty::{self, LineInput}; |
| 9 | use rsnix::{gop_print, gop_println, serial_print, serial_println}; | 31 | use rsnix::{gop_print, gop_println, serial_print, serial_println}; |
| 10 | 32 | ||
| @@ -30,137 +52,683 @@ pub fn run() -> ! { | |||
| 30 | // Read one line from keyboard (tty canonical mode) | 52 | // Read one line from keyboard (tty canonical mode) |
| 31 | match tty::read_line() { | 53 | match tty::read_line() { |
| 32 | LineInput::Interrupted => continue, // ^C — fresh prompt | 54 | LineInput::Interrupted => continue, // ^C — fresh prompt |
| 33 | LineInput::Eof => { | 55 | LineInput::Eof => continue, // ^D on an empty line — fresh prompt (no poweroff) |
| 34 | // ^D on an empty line: EOF, exit the shell. | ||
| 35 | cmd_poweroff(); | ||
| 36 | } | ||
| 37 | LineInput::Line(line) => { | 56 | LineInput::Line(line) => { |
| 38 | // Parse and execute | ||
| 39 | let trimmed = line.trim(); | 57 | let trimmed = line.trim(); |
| 40 | if trimmed.is_empty() { | 58 | if trimmed.is_empty() { |
| 41 | continue; | 59 | continue; |
| 42 | } | 60 | } |
| 61 | execute(trimmed); | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | // --------------------------------------------------------------------------- | ||
| 68 | // Diagnostics go straight to terminal + serial, never through the filter | ||
| 69 | // pipeline and never into a file sink. | ||
| 70 | // --------------------------------------------------------------------------- | ||
| 71 | |||
| 72 | fn term_err(msg: &str) { | ||
| 73 | gop_println!("{}", msg); | ||
| 74 | serial_println!("{}", msg); | ||
| 75 | } | ||
| 43 | 76 | ||
| 44 | let (cmd, args) = split_first_word(trimmed); | 77 | // --------------------------------------------------------------------------- |
| 78 | // Parsing | ||
| 79 | // --------------------------------------------------------------------------- | ||
| 80 | |||
| 81 | /// One lexical token: its text plus whether any quoting/escaping was used | ||
| 82 | /// (a token containing quoted/escaped text is never an operator or flag). | ||
| 83 | struct Token { | ||
| 84 | text: String, | ||
| 85 | literal: bool, | ||
| 86 | } | ||
| 45 | 87 | ||
| 46 | match cmd { | 88 | /// Split a command line into words, handling `"..."` groups and `\x` |
| 47 | "help" => cmd_help(), | 89 | /// escapes. Quotes are delimiters, not content; escapes prevent splitting |
| 48 | "echo" => cmd_echo(args), | 90 | /// and literalize one character (`\"` → `"`, `\ ` → space). |
| 49 | "clear" => cmd_clear(), | 91 | fn tokenize(s: &str) -> Result<Vec<Token>, &'static str> { |
| 50 | "halt" => cmd_halt(), | 92 | let mut toks: Vec<Token> = Vec::new(); |
| 51 | "poweroff" | "exit" => cmd_poweroff(), | 93 | let mut cur = String::new(); |
| 52 | "hello" => { | 94 | let mut cur_literal = false; |
| 53 | gop_println!("World!"); | 95 | let mut in_quote = false; |
| 54 | serial_println!("World!"); | 96 | let mut chars = s.chars(); |
| 97 | while let Some(c) = chars.next() { | ||
| 98 | match c { | ||
| 99 | '"' => { | ||
| 100 | cur_literal = true; | ||
| 101 | in_quote = !in_quote; | ||
| 102 | } | ||
| 103 | '\\' => { | ||
| 104 | cur_literal = true; | ||
| 105 | match chars.next() { | ||
| 106 | Some(e) => cur.push(e), | ||
| 107 | None => cur.push('\\'), // trailing backslash: literal | ||
| 108 | } | ||
| 109 | } | ||
| 110 | c if c.is_whitespace() && !in_quote => { | ||
| 111 | if !cur.is_empty() { | ||
| 112 | toks.push(Token { | ||
| 113 | text: core::mem::take(&mut cur), | ||
| 114 | literal: cur_literal, | ||
| 115 | }); | ||
| 116 | cur_literal = false; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | c => cur.push(c), | ||
| 120 | } | ||
| 121 | } | ||
| 122 | if in_quote { | ||
| 123 | return Err("unterminated quote"); | ||
| 124 | } | ||
| 125 | if !cur.is_empty() { | ||
| 126 | toks.push(Token { | ||
| 127 | text: cur, | ||
| 128 | literal: cur_literal, | ||
| 129 | }); | ||
| 130 | } | ||
| 131 | Ok(toks) | ||
| 132 | } | ||
| 133 | |||
| 134 | fn is_keyword(s: &str) -> bool { | ||
| 135 | matches!(s, "WRITING" | "WRITE" | "TO" | "USE" | "USING" | "TIL" | "UNTIL") | ||
| 136 | } | ||
| 137 | |||
| 138 | /// How the command's input is fed. Only `cat`/`hexdump`, and only when | ||
| 139 | /// path-less, may carry an input keyword. | ||
| 140 | #[derive(Clone)] | ||
| 141 | enum Input { | ||
| 142 | None, | ||
| 143 | /// `USE f` — read from a file. | ||
| 144 | Use(String), | ||
| 145 | /// `TIL m` — read from the keyboard until an exact line equals `m`. | ||
| 146 | Til(String), | ||
| 147 | /// `USE f TIL m` — read `f` until an exact line equals `m`. | ||
| 148 | UseTil(String, String), | ||
| 149 | } | ||
| 150 | |||
| 151 | impl Input { | ||
| 152 | fn present(&self) -> bool { | ||
| 153 | !matches!(self, Input::None) | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | /// A post-filter stage. Applied left to right over the line stream. | ||
| 158 | #[derive(Clone)] | ||
| 159 | enum Filter { | ||
| 160 | Head(usize), | ||
| 161 | Tail(usize), | ||
| 162 | Grep(String), | ||
| 163 | Pause, | ||
| 164 | } | ||
| 165 | |||
| 166 | /// Output destination. | ||
| 167 | #[derive(Clone)] | ||
| 168 | enum Sink { | ||
| 169 | Write(String), | ||
| 170 | Append(String), | ||
| 171 | } | ||
| 172 | |||
| 173 | struct Parsed { | ||
| 174 | cmd: String, | ||
| 175 | args: Vec<String>, | ||
| 176 | input: Input, | ||
| 177 | filters: Vec<Filter>, | ||
| 178 | output: Option<Sink>, | ||
| 179 | } | ||
| 180 | |||
| 181 | /// Parse one command line into the canonical grammar. Errors are the | ||
| 182 | /// syntax-error message to show the user. | ||
| 183 | fn parse(s: &str) -> Result<Parsed, String> { | ||
| 184 | let toks = tokenize(s).map_err(|e| e.to_string())?; | ||
| 185 | if toks.is_empty() { | ||
| 186 | return Err("empty command".into()); | ||
| 187 | } | ||
| 188 | let cmd = toks[0].text.clone(); | ||
| 189 | |||
| 190 | let mut i = 1; | ||
| 191 | let mut args: Vec<String> = Vec::new(); | ||
| 192 | |||
| 193 | let mut filters: Vec<Filter> = Vec::new(); | ||
| 194 | let mut output: Option<Sink> = None; | ||
| 195 | let mut trailing = false; // a filter flag or output keyword has been seen | ||
| 196 | let mut src: Option<String> = None; | ||
| 197 | let mut til: Option<String> = None; | ||
| 198 | let mut til_seen = false; | ||
| 199 | |||
| 200 | while i < toks.len() { | ||
| 201 | let t = &toks[i]; | ||
| 202 | let text = t.text.as_str(); | ||
| 203 | |||
| 204 | if !t.literal { | ||
| 205 | match text { | ||
| 206 | "USE" | "USING" => { | ||
| 207 | if trailing { | ||
| 208 | return Err("USE must come before the filters".into()); | ||
| 209 | } | ||
| 210 | if src.is_some() || til_seen { | ||
| 211 | return Err("duplicate input keyword".into()); | ||
| 55 | } | 212 | } |
| 56 | "info" => cmd_info(), | 213 | i += 1; |
| 57 | "mem" => cmd_mem(), | 214 | let path = toks.get(i).ok_or("USE needs a source path")?; |
| 58 | "mmap" => cmd_mmap(), | 215 | if !path.literal && is_keyword(&path.text) { |
| 59 | "stress" => cmd_stress(args), | 216 | return Err("USE needs a source path".into()); |
| 60 | "date" => cmd_date(), | ||
| 61 | "pci" => cmd_pci(), | ||
| 62 | "ls" => cmd_ls(args), | ||
| 63 | "cat" => cmd_cat(args), | ||
| 64 | "hexdump" => cmd_hexdump(args), | ||
| 65 | "mkdir" => cmd_mkdir(args), | ||
| 66 | "touch" => cmd_touch(args), | ||
| 67 | "rm" => cmd_rm(args), | ||
| 68 | "edit" => cmd_edit(args), | ||
| 69 | _ => { | ||
| 70 | gop_println!("unknown command: {}", cmd); | ||
| 71 | serial_println!("unknown command: {}", cmd); | ||
| 72 | } | 217 | } |
| 218 | src = Some(path.text.clone()); | ||
| 219 | i += 1; | ||
| 220 | continue; | ||
| 73 | } | 221 | } |
| 222 | "TIL" | "UNTIL" => { | ||
| 223 | if trailing { | ||
| 224 | return Err("TIL must come before the filters".into()); | ||
| 225 | } | ||
| 226 | if til_seen { | ||
| 227 | return Err("duplicate TIL".into()); | ||
| 228 | } | ||
| 229 | i += 1; | ||
| 230 | let m = toks.get(i).ok_or("TIL needs a marker")?; | ||
| 231 | if !m.literal && is_keyword(&m.text) { | ||
| 232 | return Err("TIL needs a marker".into()); | ||
| 233 | } | ||
| 234 | til = Some(m.text.clone()); | ||
| 235 | til_seen = true; | ||
| 236 | i += 1; | ||
| 237 | continue; | ||
| 238 | } | ||
| 239 | "WRITING" | "WRITE" | "TO" => { | ||
| 240 | if output.is_some() { | ||
| 241 | return Err("duplicate output keyword".into()); | ||
| 242 | } | ||
| 243 | i += 1; | ||
| 244 | let dst = toks.get(i).ok_or("output keyword needs a destination")?; | ||
| 245 | if !dst.literal && is_keyword(&dst.text) { | ||
| 246 | return Err("output keyword needs a destination".into()); | ||
| 247 | } | ||
| 248 | output = Some(if text == "TO" { | ||
| 249 | Sink::Append(dst.text.clone()) | ||
| 250 | } else { | ||
| 251 | Sink::Write(dst.text.clone()) | ||
| 252 | }); | ||
| 253 | trailing = true; | ||
| 254 | i += 1; | ||
| 255 | continue; | ||
| 256 | } | ||
| 257 | "-h" | "-t" => { | ||
| 258 | trailing = true; | ||
| 259 | i += 1; | ||
| 260 | let n = toks.get(i).ok_or("flag needs a count")?; | ||
| 261 | let n: usize = n.text.parse().map_err(|_| "count must be a number ≥ 1")?; | ||
| 262 | if n < 1 { | ||
| 263 | return Err("count must be ≥ 1".into()); | ||
| 264 | } | ||
| 265 | filters.push(if text == "-h" { | ||
| 266 | Filter::Head(n) | ||
| 267 | } else { | ||
| 268 | Filter::Tail(n) | ||
| 269 | }); | ||
| 270 | i += 1; | ||
| 271 | continue; | ||
| 272 | } | ||
| 273 | "-g" => { | ||
| 274 | trailing = true; | ||
| 275 | i += 1; | ||
| 276 | let pat = toks.get(i).ok_or("-g needs a pattern")?; | ||
| 277 | filters.push(Filter::Grep(pat.text.clone())); | ||
| 278 | i += 1; | ||
| 279 | continue; | ||
| 280 | } | ||
| 281 | "-p" => { | ||
| 282 | trailing = true; | ||
| 283 | filters.push(Filter::Pause); | ||
| 284 | i += 1; | ||
| 285 | continue; | ||
| 286 | } | ||
| 287 | _ => {} | ||
| 74 | } | 288 | } |
| 75 | } | 289 | } |
| 290 | |||
| 291 | // Ordinary token. Once the trailing region has started (a filter | ||
| 292 | // flag or the output keyword), stray arguments are syntax errors. | ||
| 293 | if trailing { | ||
| 294 | return Err(alloc::format!("unexpected argument '{}'", text)); | ||
| 295 | } | ||
| 296 | args.push(text.to_string()); | ||
| 297 | i += 1; | ||
| 76 | } | 298 | } |
| 299 | |||
| 300 | let input = match (src, til) { | ||
| 301 | (Some(s), Some(t)) => Input::UseTil(s, t), | ||
| 302 | (Some(s), None) => Input::Use(s), | ||
| 303 | (None, Some(t)) => Input::Til(t), | ||
| 304 | (None, None) => Input::None, | ||
| 305 | }; | ||
| 306 | |||
| 307 | Ok(Parsed { | ||
| 308 | cmd, | ||
| 309 | args, | ||
| 310 | input, | ||
| 311 | filters, | ||
| 312 | output, | ||
| 313 | }) | ||
| 77 | } | 314 | } |
| 78 | 315 | ||
| 79 | // --------------------------------------------------------------------------- | 316 | // --------------------------------------------------------------------------- |
| 80 | // Line input is handled by the tty line discipline (rsnix::tty). | 317 | // Filter pipeline + output sink |
| 81 | // --------------------------------------------------------------------------- | 318 | // --------------------------------------------------------------------------- |
| 82 | 319 | ||
| 83 | /// Split a command line into the first word and the rest. | 320 | /// Per-stage mutable state. |
| 84 | fn split_first_word(s: &str) -> (&str, &str) { | 321 | enum StageState { |
| 85 | let s = s.trim_start(); | 322 | /// Lines still allowed through before the upstream is killed. |
| 86 | if let Some(i) = s.find(char::is_whitespace) { | 323 | Head(usize), |
| 87 | (&s[..i], s[i..].trim_start()) | 324 | /// Rolling window of the last N lines. |
| 325 | Tail(VecDeque<String>), | ||
| 326 | Grep, | ||
| 327 | Pause { shown: usize, screen: usize }, | ||
| 328 | } | ||
| 329 | |||
| 330 | /// Where the filtered line stream ends up. | ||
| 331 | enum OutSink { | ||
| 332 | Term, | ||
| 333 | File { | ||
| 334 | path: String, | ||
| 335 | h: Option<usize>, | ||
| 336 | pos: u64, | ||
| 337 | overwrite: bool, | ||
| 338 | }, | ||
| 339 | } | ||
| 340 | |||
| 341 | /// Smart-case literal substring match: case-insensitive unless the pattern | ||
| 342 | /// contains an uppercase letter. | ||
| 343 | fn smart_match(pat: &str, line: &str) -> bool { | ||
| 344 | if pat.chars().any(|c| c.is_uppercase()) { | ||
| 345 | line.contains(pat) | ||
| 88 | } else { | 346 | } else { |
| 89 | (s, "") | 347 | let lp = pat.to_ascii_lowercase(); |
| 348 | let ll = line.to_ascii_lowercase(); | ||
| 349 | ll.contains(&lp) | ||
| 90 | } | 350 | } |
| 91 | } | 351 | } |
| 92 | 352 | ||
| 93 | // --------------------------------------------------------------------------- | 353 | /// Pager pause: terminal only, waits for a keypress; ^C aborts the stream. |
| 94 | // Built-in commands | 354 | fn pager_wait() -> bool { |
| 95 | // --------------------------------------------------------------------------- | 355 | gop_print!("\n— more —"); |
| 356 | match tty::read_byte() { | ||
| 357 | 0x03 => { | ||
| 358 | // ^C — abort the stream (stream-abort role). | ||
| 359 | gop_println!(); | ||
| 360 | false | ||
| 361 | } | ||
| 362 | _ => { | ||
| 363 | // Any key continues; swallow queued tail bytes (e.g. the | ||
| 364 | // rest of an arrow-key escape sequence). | ||
| 365 | while tty::try_read_byte().is_some() {} | ||
| 366 | true | ||
| 367 | } | ||
| 368 | } | ||
| 369 | } | ||
| 96 | 370 | ||
| 97 | fn cmd_help() { | 371 | /// The filter chain plus the output sink. Producers push one line at a |
| 98 | gop_println!("available commands:"); | 372 | /// time; `line()` returns false when the upstream must stop (head limit or |
| 99 | gop_println!(" help — show this message"); | 373 | /// pager ^C). At end of stream call `end()` to flush held-back tails. |
| 100 | gop_println!(" echo — echo arguments"); | 374 | struct Pipeline { |
| 101 | gop_println!(" clear — clear screen"); | 375 | stages: Vec<(Filter, StageState)>, |
| 102 | gop_println!(" hello — print 'World!'"); | 376 | sink: OutSink, |
| 103 | gop_println!(" info — show system info"); | ||
| 104 | gop_println!(" mem — show memory info"); | ||
| 105 | gop_println!(" mmap — dump the UEFI memory map"); | ||
| 106 | gop_println!(" stress — stress-test heap/page allocators"); | ||
| 107 | gop_println!(" date — show current date/time (RTC)"); | ||
| 108 | gop_println!(" pci — list PCI devices"); | ||
| 109 | gop_println!(" ls — list a directory"); | ||
| 110 | gop_println!(" cat — print files (e.g. cat /dev/rtc)"); | ||
| 111 | gop_println!(" hexdump — hex-dump a file (e.g. hexdump /dev/mem)"); | ||
| 112 | gop_println!(" mkdir — create a directory"); | ||
| 113 | gop_println!(" touch — create an empty file"); | ||
| 114 | gop_println!(" rm — remove a file or empty dir"); | ||
| 115 | gop_println!(" edit — full-screen editor (^S save, ^X exit, ^W search)"); | ||
| 116 | gop_println!(" echo > /dev/null, > file, >> file"); | ||
| 117 | gop_println!(" halt — halt the system (hlt loop)"); | ||
| 118 | gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); | ||
| 119 | gop_println!(" exit — alias for poweroff"); | ||
| 120 | serial_println!("available commands:"); | ||
| 121 | serial_println!( | ||
| 122 | " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, edit, halt, poweroff, exit" | ||
| 123 | ); | ||
| 124 | } | 377 | } |
| 125 | 378 | ||
| 126 | fn cmd_echo(args: &str) { | 379 | impl Pipeline { |
| 127 | // Redirection: `text > path` (overwrite) or `text >> path` (append). | 380 | fn new(filters: &[Filter], output: &Option<Sink>) -> Self { |
| 128 | if let Some(idx) = args.find('>') { | 381 | let file_sink = output.is_some(); |
| 129 | let append = args[idx..].starts_with(">>"); | 382 | let mut stages: Vec<(Filter, StageState)> = Vec::new(); |
| 130 | let text = &args[..idx]; | 383 | for f in filters { |
| 131 | let path = args[idx + 1 + append as usize..].trim(); | 384 | // With a file sink the pager is a no-op. |
| 132 | let path = path.split_whitespace().next().unwrap_or(""); | 385 | if file_sink && matches!(f, Filter::Pause) { |
| 133 | if !path.is_empty() { | 386 | continue; |
| 134 | let content = alloc::format!("{}\n", text.trim_end()); | 387 | } |
| 135 | let result = rsnix::fs::ensure_file(path).and_then(|h| { | 388 | let st = match f { |
| 136 | if append { | 389 | Filter::Head(n) => StageState::Head(*n), |
| 137 | rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)).map(|_| ()) | 390 | Filter::Tail(n) => StageState::Tail(VecDeque::with_capacity(*n)), |
| 138 | } else { | 391 | Filter::Grep(_) => StageState::Grep, |
| 392 | Filter::Pause => StageState::Pause { | ||
| 393 | shown: 0, | ||
| 394 | screen: rsnix::vt::size().0.saturating_sub(1).max(1), | ||
| 395 | }, | ||
| 396 | }; | ||
| 397 | stages.push((f.clone(), st)); | ||
| 398 | } | ||
| 399 | let sink = match output { | ||
| 400 | Some(Sink::Write(p)) => OutSink::File { | ||
| 401 | path: p.clone(), | ||
| 402 | h: None, | ||
| 403 | pos: 0, | ||
| 404 | overwrite: true, | ||
| 405 | }, | ||
| 406 | Some(Sink::Append(p)) => OutSink::File { | ||
| 407 | path: p.clone(), | ||
| 408 | h: None, | ||
| 409 | pos: 0, | ||
| 410 | overwrite: false, | ||
| 411 | }, | ||
| 412 | None => OutSink::Term, | ||
| 413 | }; | ||
| 414 | Pipeline { stages, sink } | ||
| 415 | } | ||
| 416 | |||
| 417 | /// Push one line through the stages. Returns false when the producer | ||
| 418 | /// must stop (head killed the stream, or ^C at the pager). | ||
| 419 | fn line(&mut self, l: &str) -> bool { | ||
| 420 | self.push_through(0, l) | ||
| 421 | } | ||
| 422 | |||
| 423 | /// Run one line through the stages starting at `start`, delivering to | ||
| 424 | /// the sink. `start` lets `finish()` replay a drained tail window | ||
| 425 | /// through only the stages after it. | ||
| 426 | fn push_through(&mut self, start: usize, l: &str) -> bool { | ||
| 427 | let mut cur = alloc::vec![l.to_string()]; | ||
| 428 | for (i, (f, st)) in self.stages.iter_mut().enumerate() { | ||
| 429 | if i < start { | ||
| 430 | continue; | ||
| 431 | } | ||
| 432 | let mut out = Vec::new(); | ||
| 433 | match f { | ||
| 434 | Filter::Head(_) => { | ||
| 435 | let StageState::Head(rem) = st else { | ||
| 436 | unreachable!("stage state does not match filter") | ||
| 437 | }; | ||
| 438 | if *rem == 0 { | ||
| 439 | // Head limit reached: kill the upstream. The line | ||
| 440 | // that would have overflowed is dropped. | ||
| 441 | return false; | ||
| 442 | } | ||
| 443 | for l in cur.drain(..) { | ||
| 444 | *rem -= 1; | ||
| 445 | out.push(l); | ||
| 446 | } | ||
| 447 | } | ||
| 448 | Filter::Tail(n) => { | ||
| 449 | let StageState::Tail(buf) = st else { | ||
| 450 | unreachable!("stage state does not match filter") | ||
| 451 | }; | ||
| 452 | for l in cur.drain(..) { | ||
| 453 | if buf.len() == *n { | ||
| 454 | buf.pop_front(); | ||
| 455 | } | ||
| 456 | buf.push_back(l); | ||
| 457 | } | ||
| 458 | // Held back — nothing reaches the downstream yet. | ||
| 459 | return true; | ||
| 460 | } | ||
| 461 | Filter::Grep(pat) => { | ||
| 462 | let StageState::Grep = st else { | ||
| 463 | unreachable!("stage state does not match filter") | ||
| 464 | }; | ||
| 465 | for l in cur.drain(..) { | ||
| 466 | if smart_match(pat, &l) { | ||
| 467 | out.push(l); | ||
| 468 | } | ||
| 469 | } | ||
| 470 | } | ||
| 471 | Filter::Pause => { | ||
| 472 | let StageState::Pause { shown, screen } = st else { | ||
| 473 | unreachable!("stage state does not match filter") | ||
| 474 | }; | ||
| 475 | for l in cur.drain(..) { | ||
| 476 | if *shown > 0 && *shown % *screen == 0 | ||
| 477 | && !pager_wait() { | ||
| 478 | return false; | ||
| 479 | } | ||
| 480 | *shown += 1; | ||
| 481 | out.push(l); | ||
| 482 | } | ||
| 483 | } | ||
| 484 | } | ||
| 485 | cur = out; | ||
| 486 | } | ||
| 487 | for l in &cur { | ||
| 488 | self.emit(l); | ||
| 489 | } | ||
| 490 | true | ||
| 491 | } | ||
| 492 | |||
| 493 | /// Flush held-back tail windows through the remaining stages. Called | ||
| 494 | /// at end of stream: EOF, ^C, or after a head kill. | ||
| 495 | fn finish(&mut self) { | ||
| 496 | loop { | ||
| 497 | let mut any = false; | ||
| 498 | for i in 0..self.stages.len() { | ||
| 499 | let drained: Vec<String> = match &mut self.stages[i] { | ||
| 500 | (Filter::Tail(_), StageState::Tail(buf)) if !buf.is_empty() => { | ||
| 501 | buf.drain(..).collect() | ||
| 502 | } | ||
| 503 | _ => continue, | ||
| 504 | }; | ||
| 505 | any = true; | ||
| 506 | for l in drained { | ||
| 507 | self.push_through(i + 1, &l); | ||
| 508 | } | ||
| 509 | } | ||
| 510 | if !any { | ||
| 511 | break; | ||
| 512 | } | ||
| 513 | } | ||
| 514 | } | ||
| 515 | |||
| 516 | /// End of stream: flush tails, done. File writes are synchronous. | ||
| 517 | fn end(&mut self) { | ||
| 518 | self.finish(); | ||
| 519 | } | ||
| 520 | |||
| 521 | fn emit(&mut self, l: &str) { | ||
| 522 | match &self.sink { | ||
| 523 | OutSink::Term => { | ||
| 524 | gop_println!("{}", l); | ||
| 525 | serial_println!("{}", l); | ||
| 526 | } | ||
| 527 | OutSink::File { .. } => {} | ||
| 528 | } | ||
| 529 | if let OutSink::File { h: None, .. } = &self.sink { | ||
| 530 | self.open_sink(); | ||
| 531 | } | ||
| 532 | if let OutSink::File { h: Some(h), pos, .. } = &mut self.sink { | ||
| 533 | let mut bytes = l.as_bytes().to_vec(); | ||
| 534 | bytes.push(b'\n'); | ||
| 535 | if let Ok(n) = rsnix::fs::write(*h, &bytes, *pos) { | ||
| 536 | *pos += n as u64; | ||
| 537 | } | ||
| 538 | } | ||
| 539 | } | ||
| 540 | |||
| 541 | /// Lazily open the file sink on first output (a command that emits | ||
| 542 | /// nothing touches no file). WRITING truncates and writes from offset | ||
| 543 | /// 0 (the node's cached size is stale after truncation); TO appends at | ||
| 544 | /// the file's current size. | ||
| 545 | fn open_sink(&mut self) { | ||
| 546 | let (path, overwrite) = match &self.sink { | ||
| 547 | OutSink::File { path, overwrite, .. } => (path.clone(), *overwrite), | ||
| 548 | _ => return, | ||
| 549 | }; | ||
| 550 | match rsnix::fs::ensure_file(&path) { | ||
| 551 | Ok(h) => { | ||
| 552 | let pos = if overwrite { | ||
| 139 | let _ = rsnix::fs::set_len(h, 0); | 553 | let _ = rsnix::fs::set_len(h, 0); |
| 140 | rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ()) | 554 | 0 |
| 555 | } else { | ||
| 556 | rsnix::fs::size(h) | ||
| 557 | }; | ||
| 558 | self.sink = OutSink::File { | ||
| 559 | path, | ||
| 560 | h: Some(h), | ||
| 561 | pos, | ||
| 562 | overwrite, | ||
| 563 | }; | ||
| 564 | } | ||
| 565 | Err(()) => { | ||
| 566 | term_err(&alloc::format!("cannot write '{}'", path)); | ||
| 567 | self.sink = OutSink::Term; | ||
| 568 | } | ||
| 569 | } | ||
| 570 | } | ||
| 571 | } | ||
| 572 | |||
| 573 | /// Emit every line of a pre-built multi-line string (interior blank lines | ||
| 574 | /// preserved; a trailing newline does not produce an extra empty line). | ||
| 575 | fn emit_string(pipe: &mut Pipeline, s: &str) { | ||
| 576 | let mut rest = s; | ||
| 577 | loop { | ||
| 578 | match rest.find('\n') { | ||
| 579 | Some(idx) => { | ||
| 580 | if !pipe.line(&rest[..idx]) { | ||
| 581 | return; | ||
| 141 | } | 582 | } |
| 142 | }); | 583 | rest = &rest[idx + 1..]; |
| 143 | match result { | 584 | } |
| 144 | Ok(()) => {} | 585 | None => { |
| 145 | Err(()) => { | 586 | if !rest.is_empty() { |
| 146 | gop_println!("echo: cannot write '{}'", path); | 587 | let _ = pipe.line(rest); |
| 147 | serial_println!("echo: cannot write '{}'", path); | ||
| 148 | } | 588 | } |
| 589 | return; | ||
| 149 | } | 590 | } |
| 591 | } | ||
| 592 | } | ||
| 593 | } | ||
| 594 | |||
| 595 | // --------------------------------------------------------------------------- | ||
| 596 | // Execution | ||
| 597 | // --------------------------------------------------------------------------- | ||
| 598 | |||
| 599 | /// Commands that may consume an input stream (`USE`/`TIL`). | ||
| 600 | fn accepts_input(cmd: &str) -> bool { | ||
| 601 | matches!(cmd, "cat" | "hexdump") | ||
| 602 | } | ||
| 603 | |||
| 604 | fn execute(s: &str) { | ||
| 605 | let parsed = match parse(s) { | ||
| 606 | Ok(p) => p, | ||
| 607 | Err(e) => { | ||
| 608 | term_err(&e); | ||
| 609 | return; | ||
| 610 | } | ||
| 611 | }; | ||
| 612 | let cmd = parsed.cmd.as_str(); | ||
| 613 | |||
| 614 | if parsed.input.present() { | ||
| 615 | if !accepts_input(cmd) { | ||
| 616 | term_err(&alloc::format!("{}: does not accept input", cmd)); | ||
| 617 | return; | ||
| 618 | } | ||
| 619 | if !parsed.args.is_empty() { | ||
| 620 | term_err(&alloc::format!( | ||
| 621 | "{}: input keyword with path arguments is ambiguous", | ||
| 622 | cmd | ||
| 623 | )); | ||
| 150 | return; | 624 | return; |
| 151 | } | 625 | } |
| 152 | } | 626 | } |
| 153 | gop_println!("{}", args); | 627 | |
| 154 | serial_println!("{}", args); | 628 | match cmd { |
| 629 | "help" => cmd_help(&parsed), | ||
| 630 | "echo" => cmd_echo(&parsed), | ||
| 631 | "clear" => cmd_clear(), | ||
| 632 | "halt" => cmd_halt(), | ||
| 633 | "poweroff" | "exit" => cmd_poweroff(), | ||
| 634 | "hello" => cmd_hello(&parsed), | ||
| 635 | "info" => cmd_info(&parsed), | ||
| 636 | "mem" => cmd_mem(&parsed), | ||
| 637 | "mmap" => cmd_mmap(&parsed), | ||
| 638 | "stress" => cmd_stress(&parsed), | ||
| 639 | "date" => cmd_date(&parsed), | ||
| 640 | "pci" => cmd_pci(&parsed), | ||
| 641 | "ls" => cmd_ls(&parsed), | ||
| 642 | "cat" => cmd_cat(&parsed), | ||
| 643 | "hexdump" => cmd_hexdump(&parsed), | ||
| 644 | "mkdir" => cmd_mkdir(&parsed), | ||
| 645 | "touch" => cmd_touch(&parsed), | ||
| 646 | "rm" => cmd_rm(&parsed), | ||
| 647 | "edit" => cmd_edit(&parsed), | ||
| 648 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), | ||
| 649 | } | ||
| 650 | } | ||
| 651 | |||
| 652 | // --------------------------------------------------------------------------- | ||
| 653 | // Built-in commands | ||
| 654 | // --------------------------------------------------------------------------- | ||
| 655 | |||
| 656 | fn cmd_help(_p: &Parsed) { | ||
| 657 | let mut pipe = Pipeline::new(&_p.filters, &_p.output); | ||
| 658 | let text = "available commands: | ||
| 659 | help — show this message | ||
| 660 | echo — echo arguments | ||
| 661 | clear — clear screen | ||
| 662 | hello — print 'World!' | ||
| 663 | info — show system info | ||
| 664 | mem — show memory info | ||
| 665 | mmap — dump the UEFI memory map | ||
| 666 | stress — stress-test heap/page allocators | ||
| 667 | date — show current date/time (RTC) | ||
| 668 | pci — list PCI devices | ||
| 669 | ls — list a directory | ||
| 670 | cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) | ||
| 671 | hexdump — hex-dump a file or input stream | ||
| 672 | mkdir — create a directory | ||
| 673 | touch — create an empty file | ||
| 674 | rm — remove a file or empty dir | ||
| 675 | edit — full-screen editor (^S save, ^X exit, ^W search) | ||
| 676 | halt — halt the system (hlt loop) | ||
| 677 | poweroff — shut down via ACPI (ResetSystem) | ||
| 678 | exit — alias for poweroff | ||
| 679 | |||
| 680 | redirection keywords (uppercase, after args): | ||
| 681 | USE/USING <src> read command input from a file (cat/hexdump only) | ||
| 682 | TIL/UNTIL <marker> read input until an exact marker line (heredoc) | ||
| 683 | WRITING/WRITE <dst> write output to a file (overwrite) | ||
| 684 | TO <dst> append output to a file | ||
| 685 | a file sink replaces the screen; > >> < << are gone (> is plain text) | ||
| 686 | |||
| 687 | post-filter flags (trailing, applied left to right): | ||
| 688 | -h N first N lines (kills the command after N) | ||
| 689 | -t N last N lines (shows nothing until the stream ends; ^C flushes) | ||
| 690 | -g PAT smart-case literal substring match (repeatable) | ||
| 691 | -p pause at each screenful (more-style; ^C aborts) | ||
| 692 | |||
| 693 | quoting: \"...\" groups text; \\x escapes one char. Quoted text is never | ||
| 694 | an operator: echo \"WRITING\" prints WRITING. | ||
| 695 | ^D = EOF; ^C cancels a line or aborts a stream. poweroff/exit shut down."; | ||
| 696 | emit_string(&mut pipe, text); | ||
| 697 | pipe.end(); | ||
| 698 | } | ||
| 699 | |||
| 700 | fn cmd_echo(p: &Parsed) { | ||
| 701 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 702 | let line = p.args.join(" "); | ||
| 703 | let _ = pipe.line(&line); | ||
| 704 | pipe.end(); | ||
| 705 | } | ||
| 706 | |||
| 707 | fn cmd_hello(p: &Parsed) { | ||
| 708 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 709 | let _ = pipe.line("World!"); | ||
| 710 | pipe.end(); | ||
| 711 | } | ||
| 712 | |||
| 713 | fn cmd_clear() { | ||
| 714 | rsnix::gop::clear_screen(); | ||
| 715 | } | ||
| 716 | |||
| 717 | fn cmd_info(p: &Parsed) { | ||
| 718 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 719 | let mut s = String::from("rs-dos v"); | ||
| 720 | s.push_str(env!("CARGO_PKG_VERSION")); | ||
| 721 | s.push_str(" — rsnix kernel"); | ||
| 722 | let _ = pipe.line(&s); | ||
| 723 | let _ = pipe.line("arch: x86_64, freestanding, no userspace"); | ||
| 724 | pipe.end(); | ||
| 155 | } | 725 | } |
| 156 | 726 | ||
| 157 | fn cmd_ls(args: &str) { | 727 | fn cmd_ls(p: &Parsed) { |
| 158 | let path = args.split_whitespace().next().unwrap_or("/"); | 728 | let path = p.args.first().map(|s| s.as_str()).unwrap_or("/"); |
| 159 | let mut out = alloc::string::String::new(); | 729 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 160 | match rsnix::fs::resolve(path) { | 730 | match rsnix::fs::resolve(path) { |
| 161 | None => { | 731 | None => term_err(&alloc::format!("ls: {}: no such file or directory", path)), |
| 162 | out.push_str(&alloc::format!("ls: {}: no such file or directory\n", path)); | ||
| 163 | } | ||
| 164 | Some(h) => { | 732 | Some(h) => { |
| 165 | let list = if rsnix::fs::kind(h) == rsnix::fs::FileType::Dir { | 733 | let list = if rsnix::fs::kind(h) == rsnix::fs::FileType::Dir { |
| 166 | rsnix::fs::children(h) | 734 | rsnix::fs::children(h) |
| @@ -172,208 +740,476 @@ fn cmd_ls(args: &str) { | |||
| 172 | )] | 740 | )] |
| 173 | }; | 741 | }; |
| 174 | for (name, kind, size) in list { | 742 | for (name, kind, size) in list { |
| 175 | match kind { | 743 | let l = match kind { |
| 176 | rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)), | 744 | rsnix::fs::FileType::Dir => alloc::format!("{}/", name), |
| 177 | rsnix::fs::FileType::Char => out.push_str(&alloc::format!("{}@\n", name)), | 745 | rsnix::fs::FileType::Char => alloc::format!("{}@", name), |
| 178 | rsnix::fs::FileType::Regular => { | 746 | rsnix::fs::FileType::Regular => alloc::format!("{} {} B", name, size), |
| 179 | out.push_str(&alloc::format!("{} {} B\n", name, size)) | 747 | }; |
| 748 | if !pipe.line(&l) { | ||
| 749 | break; | ||
| 750 | } | ||
| 751 | } | ||
| 752 | } | ||
| 753 | } | ||
| 754 | pipe.end(); | ||
| 755 | } | ||
| 756 | |||
| 757 | // --------------------------------------------------------------------------- | ||
| 758 | // cat — file and stream consumers | ||
| 759 | // --------------------------------------------------------------------------- | ||
| 760 | |||
| 761 | fn cmd_cat(p: &Parsed) { | ||
| 762 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 763 | match &p.input { | ||
| 764 | Input::None => { | ||
| 765 | if p.args.is_empty() { | ||
| 766 | // Path-less `cat` with no input keyword reads the keyboard | ||
| 767 | // until ^D. | ||
| 768 | cat_stdin(&mut pipe); | ||
| 769 | } else { | ||
| 770 | for path in &p.args { | ||
| 771 | if !cat_file(&mut pipe, path) { | ||
| 772 | break; | ||
| 180 | } | 773 | } |
| 181 | } | 774 | } |
| 182 | } | 775 | } |
| 183 | } | 776 | } |
| 777 | Input::Use(path) => { | ||
| 778 | cat_file(&mut pipe, path); | ||
| 779 | } | ||
| 780 | Input::Til(marker) => { | ||
| 781 | cat_stdin_until(&mut pipe, marker); | ||
| 782 | } | ||
| 783 | Input::UseTil(path, marker) => { | ||
| 784 | cat_file_until(&mut pipe, path, marker); | ||
| 785 | } | ||
| 184 | } | 786 | } |
| 185 | gop_print!("{}", out); | 787 | pipe.end(); |
| 186 | serial_print!("{}", out); | ||
| 187 | } | 788 | } |
| 188 | 789 | ||
| 189 | fn cmd_cat(args: &str) { | 790 | fn cat_stdin(pipe: &mut Pipeline) -> bool { |
| 190 | let paths: alloc::vec::Vec<&str> = args.split_whitespace().collect(); | 791 | loop { |
| 191 | if paths.is_empty() { | 792 | match tty::read_line() { |
| 192 | gop_println!("usage: cat <path> [path...]"); | 793 | LineInput::Line(l) => { |
| 193 | serial_println!("usage: cat <path> [path...]"); | 794 | if !pipe.line(l.as_str()) { |
| 194 | return; | 795 | return false; |
| 796 | } | ||
| 797 | } | ||
| 798 | LineInput::Interrupted => return false, // ^C aborts the stream | ||
| 799 | LineInput::Eof => return true, // ^D = EOF | ||
| 800 | } | ||
| 801 | } | ||
| 802 | } | ||
| 803 | |||
| 804 | fn cat_stdin_until(pipe: &mut Pipeline, marker: &str) -> bool { | ||
| 805 | loop { | ||
| 806 | match tty::read_line() { | ||
| 807 | LineInput::Line(l) => { | ||
| 808 | // Marker line is not part of the input. | ||
| 809 | if l.as_str() == marker { | ||
| 810 | return true; | ||
| 811 | } | ||
| 812 | if !pipe.line(l.as_str()) { | ||
| 813 | return false; | ||
| 814 | } | ||
| 815 | } | ||
| 816 | LineInput::Interrupted => return false, | ||
| 817 | LineInput::Eof => return true, | ||
| 818 | } | ||
| 195 | } | 819 | } |
| 820 | } | ||
| 821 | |||
| 822 | fn cat_file(pipe: &mut Pipeline, path: &str) -> bool { | ||
| 823 | let h = match rsnix::fs::resolve(path) { | ||
| 824 | Some(h) => h, | ||
| 825 | None => { | ||
| 826 | term_err(&alloc::format!("cat: {}: no such file", path)); | ||
| 827 | return true; // keep going with the remaining paths | ||
| 828 | } | ||
| 829 | }; | ||
| 196 | let mut buf = [0u8; 512]; | 830 | let mut buf = [0u8; 512]; |
| 197 | for p in paths { | 831 | let mut pos = 0u64; |
| 198 | match rsnix::fs::resolve(p) { | 832 | let mut pending = String::new(); |
| 199 | None => { | 833 | loop { |
| 200 | let msg = alloc::format!("cat: {}: no such file\n", p); | 834 | if tty::interrupt_pressed() { |
| 201 | gop_print!("{}", msg); | 835 | return false; // ^C aborts everything |
| 202 | serial_print!("{}", msg); | 836 | } |
| 837 | match rsnix::fs::read(h, &mut buf, pos) { | ||
| 838 | Ok(0) | Err(_) => break, | ||
| 839 | Ok(n) => { | ||
| 840 | pos += n as u64; | ||
| 841 | pending.push_str(&String::from_utf8_lossy(&buf[..n])); | ||
| 842 | while let Some(idx) = pending.find('\n') { | ||
| 843 | let line: String = pending.drain(..=idx).collect(); | ||
| 844 | let line = line.trim_end_matches('\n'); | ||
| 845 | if !pipe.line(line) { | ||
| 846 | return false; | ||
| 847 | } | ||
| 848 | } | ||
| 203 | } | 849 | } |
| 204 | Some(h) => { | 850 | } |
| 205 | let mut pos = 0u64; | 851 | } |
| 206 | loop { | 852 | if !pending.is_empty() && !pipe.line(&pending) { |
| 207 | if rsnix::tty::interrupt_pressed() { | 853 | return false; |
| 208 | gop_println!("^C"); | 854 | } |
| 209 | serial_println!("^C"); | 855 | true |
| 210 | return; | 856 | } |
| 857 | |||
| 858 | fn cat_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> bool { | ||
| 859 | let h = match rsnix::fs::resolve(path) { | ||
| 860 | Some(h) => h, | ||
| 861 | None => { | ||
| 862 | term_err(&alloc::format!("cat: {}: no such file", path)); | ||
| 863 | return true; | ||
| 864 | } | ||
| 865 | }; | ||
| 866 | let mut buf = [0u8; 512]; | ||
| 867 | let mut pos = 0u64; | ||
| 868 | let mut pending = String::new(); | ||
| 869 | let mut stopped = false; | ||
| 870 | 'outer: loop { | ||
| 871 | if tty::interrupt_pressed() { | ||
| 872 | return false; | ||
| 873 | } | ||
| 874 | match rsnix::fs::read(h, &mut buf, pos) { | ||
| 875 | Ok(0) | Err(_) => break, | ||
| 876 | Ok(n) => { | ||
| 877 | pos += n as u64; | ||
| 878 | pending.push_str(&String::from_utf8_lossy(&buf[..n])); | ||
| 879 | while let Some(idx) = pending.find('\n') { | ||
| 880 | let line: String = pending.drain(..=idx).collect(); | ||
| 881 | let line = line.trim_end_matches('\n'); | ||
| 882 | // Marker line and everything after is discarded. | ||
| 883 | if line == marker { | ||
| 884 | stopped = true; | ||
| 885 | break 'outer; | ||
| 211 | } | 886 | } |
| 212 | match rsnix::fs::read(h, &mut buf, pos) { | 887 | if !pipe.line(line) { |
| 213 | Ok(0) | Err(_) => break, | 888 | return false; |
| 214 | Ok(n) => { | ||
| 215 | let chunk = core::str::from_utf8(&buf[..n]).unwrap_or("?"); | ||
| 216 | gop_print!("{}", chunk); | ||
| 217 | serial_print!("{}", chunk); | ||
| 218 | pos += n as u64; | ||
| 219 | } | ||
| 220 | } | 889 | } |
| 221 | } | 890 | } |
| 222 | } | 891 | } |
| 223 | } | 892 | } |
| 224 | } | 893 | } |
| 894 | if !stopped && !pending.is_empty() && pending != marker && !pipe.line(&pending) { | ||
| 895 | return false; | ||
| 896 | } | ||
| 897 | true | ||
| 225 | } | 898 | } |
| 226 | 899 | ||
| 227 | fn cmd_hexdump(args: &str) { | 900 | // --------------------------------------------------------------------------- |
| 228 | let path = args.split_whitespace().next().unwrap_or(""); | 901 | // hexdump — hex-dump files and input streams |
| 229 | if path.is_empty() { | 902 | // --------------------------------------------------------------------------- |
| 230 | gop_println!("usage: hexdump <path>"); | 903 | |
| 231 | serial_println!("usage: hexdump <path>"); | 904 | struct HexCtx<'a> { |
| 232 | return; | 905 | pipe: &'a mut Pipeline, |
| 906 | pos: u64, | ||
| 907 | row: [u8; 16], | ||
| 908 | row_len: usize, | ||
| 909 | alive: bool, | ||
| 910 | } | ||
| 911 | |||
| 912 | fn format_row(addr: u64, row: &[u8; 16]) -> String { | ||
| 913 | let mut out = String::new(); | ||
| 914 | use core::fmt::Write as _; | ||
| 915 | let _ = core::write!(out, "{:08x} ", addr); | ||
| 916 | for &b in row { | ||
| 917 | let _ = core::write!(out, "{:02x} ", b); | ||
| 918 | } | ||
| 919 | out.push_str(" |"); | ||
| 920 | for &b in row { | ||
| 921 | out.push(if b.is_ascii_graphic() || b == b' ' { | ||
| 922 | b as char | ||
| 923 | } else { | ||
| 924 | '.' | ||
| 925 | }); | ||
| 926 | } | ||
| 927 | out.push('|'); | ||
| 928 | out | ||
| 929 | } | ||
| 930 | |||
| 931 | fn format_partial(addr: u64, row: &[u8]) -> String { | ||
| 932 | let mut out = String::new(); | ||
| 933 | use core::fmt::Write as _; | ||
| 934 | let _ = core::write!(out, "{:08x} ", addr); | ||
| 935 | for i in 0..16 { | ||
| 936 | if i < row.len() { | ||
| 937 | let _ = core::write!(out, "{:02x} ", row[i]); | ||
| 938 | } else { | ||
| 939 | out.push_str(" "); | ||
| 940 | } | ||
| 941 | } | ||
| 942 | out.push_str(" |"); | ||
| 943 | for &b in row { | ||
| 944 | out.push(if b.is_ascii_graphic() || b == b' ' { | ||
| 945 | b as char | ||
| 946 | } else { | ||
| 947 | '.' | ||
| 948 | }); | ||
| 949 | } | ||
| 950 | out.push('|'); | ||
| 951 | out | ||
| 952 | } | ||
| 953 | |||
| 954 | impl HexCtx<'_> { | ||
| 955 | fn feed(&mut self, data: &[u8]) { | ||
| 956 | if !self.alive { | ||
| 957 | return; | ||
| 958 | } | ||
| 959 | for &b in data { | ||
| 960 | self.row[self.row_len] = b; | ||
| 961 | self.row_len += 1; | ||
| 962 | if self.row_len == 16 { | ||
| 963 | let line = format_row(self.pos, &self.row); | ||
| 964 | self.alive = self.pipe.line(&line); | ||
| 965 | self.pos += 16; | ||
| 966 | self.row_len = 0; | ||
| 967 | if !self.alive { | ||
| 968 | return; | ||
| 969 | } | ||
| 970 | } | ||
| 971 | } | ||
| 233 | } | 972 | } |
| 973 | |||
| 974 | /// Emit the trailing partial row, if any. | ||
| 975 | fn flush(&mut self) { | ||
| 976 | if self.alive && self.row_len > 0 { | ||
| 977 | let line = format_partial(self.pos, &self.row[..self.row_len]); | ||
| 978 | let _ = self.pipe.line(&line); | ||
| 979 | } | ||
| 980 | } | ||
| 981 | } | ||
| 982 | |||
| 983 | fn cmd_hexdump(p: &Parsed) { | ||
| 984 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 985 | match &p.input { | ||
| 986 | Input::None => { | ||
| 987 | if p.args.is_empty() { | ||
| 988 | hexdump_stdin(&mut pipe); | ||
| 989 | } else { | ||
| 990 | for path in &p.args { | ||
| 991 | if !hexdump_file(&mut pipe, path) { | ||
| 992 | break; | ||
| 993 | } | ||
| 994 | } | ||
| 995 | } | ||
| 996 | } | ||
| 997 | Input::Use(path) => { | ||
| 998 | hexdump_file(&mut pipe, path); | ||
| 999 | } | ||
| 1000 | Input::Til(marker) => { | ||
| 1001 | hexdump_stdin_until(&mut pipe, marker); | ||
| 1002 | } | ||
| 1003 | Input::UseTil(path, marker) => { | ||
| 1004 | hexdump_file_until(&mut pipe, path, marker); | ||
| 1005 | } | ||
| 1006 | } | ||
| 1007 | pipe.end(); | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | fn hexdump_file(pipe: &mut Pipeline, path: &str) -> bool { | ||
| 234 | let h = match rsnix::fs::resolve(path) { | 1011 | let h = match rsnix::fs::resolve(path) { |
| 235 | Some(h) => h, | 1012 | Some(h) => h, |
| 236 | None => { | 1013 | None => { |
| 237 | gop_println!("hexdump: {}: no such file", path); | 1014 | term_err(&alloc::format!("hexdump: {}: no such file", path)); |
| 238 | serial_println!("hexdump: {}: no such file", path); | 1015 | return true; |
| 239 | return; | ||
| 240 | } | 1016 | } |
| 241 | }; | 1017 | }; |
| 1018 | let mut ctx = HexCtx { | ||
| 1019 | pipe, | ||
| 1020 | pos: 0, | ||
| 1021 | row: [0u8; 16], | ||
| 1022 | row_len: 0, | ||
| 1023 | alive: true, | ||
| 1024 | }; | ||
| 242 | let mut buf = [0u8; 512]; | 1025 | let mut buf = [0u8; 512]; |
| 243 | let mut pos = 0u64; | 1026 | let mut pos = 0u64; |
| 244 | loop { | 1027 | loop { |
| 245 | if rsnix::tty::interrupt_pressed() { | 1028 | if tty::interrupt_pressed() { |
| 246 | gop_println!("^C"); | 1029 | ctx.flush(); |
| 247 | serial_println!("^C"); | 1030 | return false; |
| 248 | return; | ||
| 249 | } | 1031 | } |
| 250 | let n = match rsnix::fs::read(h, &mut buf, pos) { | 1032 | match rsnix::fs::read(h, &mut buf, pos) { |
| 251 | Ok(0) | Err(_) => break, | 1033 | Ok(0) | Err(_) => break, |
| 252 | Ok(n) => n, | 1034 | Ok(n) => { |
| 253 | }; | 1035 | pos += n as u64; |
| 254 | let mut out = alloc::string::String::new(); | 1036 | ctx.feed(&buf[..n]); |
| 255 | use core::fmt::Write as _; | 1037 | if !ctx.alive { |
| 256 | for i in (0..n).step_by(16) { | 1038 | break; |
| 257 | let _ = core::write!(out, "{:08x} ", pos + i as u64); | ||
| 258 | for j in 0..16 { | ||
| 259 | if i + j < n { | ||
| 260 | let _ = core::write!(out, "{:02x} ", buf[i + j]); | ||
| 261 | } else { | ||
| 262 | out.push_str(" "); | ||
| 263 | } | 1039 | } |
| 264 | } | 1040 | } |
| 265 | out.push_str(" |"); | 1041 | } |
| 266 | for j in 0..16 { | 1042 | } |
| 267 | if i + j >= n { | 1043 | ctx.flush(); |
| 1044 | ctx.alive | ||
| 1045 | } | ||
| 1046 | |||
| 1047 | fn hexdump_stdin(pipe: &mut Pipeline) { | ||
| 1048 | let mut ctx = HexCtx { | ||
| 1049 | pipe, | ||
| 1050 | pos: 0, | ||
| 1051 | row: [0u8; 16], | ||
| 1052 | row_len: 0, | ||
| 1053 | alive: true, | ||
| 1054 | }; | ||
| 1055 | loop { | ||
| 1056 | match tty::read_line() { | ||
| 1057 | LineInput::Line(l) => { | ||
| 1058 | ctx.feed(l.as_str().as_bytes()); | ||
| 1059 | if !ctx.alive { | ||
| 1060 | break; | ||
| 1061 | } | ||
| 1062 | ctx.feed(b"\n"); | ||
| 1063 | if !ctx.alive { | ||
| 268 | break; | 1064 | break; |
| 269 | } | 1065 | } |
| 270 | let b = buf[i + j]; | ||
| 271 | out.push(if b.is_ascii_graphic() || b == b' ' { | ||
| 272 | b as char | ||
| 273 | } else { | ||
| 274 | '.' | ||
| 275 | }); | ||
| 276 | } | 1066 | } |
| 277 | out.push_str("|\n"); | 1067 | LineInput::Interrupted => break, |
| 1068 | LineInput::Eof => break, | ||
| 278 | } | 1069 | } |
| 279 | gop_print!("{}", out); | ||
| 280 | serial_print!("{}", out); | ||
| 281 | pos += n as u64; | ||
| 282 | } | 1070 | } |
| 1071 | ctx.flush(); | ||
| 283 | } | 1072 | } |
| 284 | 1073 | ||
| 285 | // --------------------------------------------------------------------------- | 1074 | fn hexdump_stdin_until(pipe: &mut Pipeline, marker: &str) { |
| 286 | // edit — run the built-in editor (rsdosedit) on a file | 1075 | let mut ctx = HexCtx { |
| 287 | // --------------------------------------------------------------------------- | 1076 | pipe, |
| 1077 | pos: 0, | ||
| 1078 | row: [0u8; 16], | ||
| 1079 | row_len: 0, | ||
| 1080 | alive: true, | ||
| 1081 | }; | ||
| 1082 | loop { | ||
| 1083 | match tty::read_line() { | ||
| 1084 | LineInput::Line(l) => { | ||
| 1085 | if l.as_str() == marker { | ||
| 1086 | break; | ||
| 1087 | } | ||
| 1088 | ctx.feed(l.as_str().as_bytes()); | ||
| 1089 | if !ctx.alive { | ||
| 1090 | break; | ||
| 1091 | } | ||
| 1092 | ctx.feed(b"\n"); | ||
| 1093 | if !ctx.alive { | ||
| 1094 | break; | ||
| 1095 | } | ||
| 1096 | } | ||
| 1097 | LineInput::Interrupted => break, | ||
| 1098 | LineInput::Eof => break, | ||
| 1099 | } | ||
| 1100 | } | ||
| 1101 | ctx.flush(); | ||
| 1102 | } | ||
| 288 | 1103 | ||
| 289 | fn cmd_edit(args: &str) { | 1104 | fn hexdump_file_until(pipe: &mut Pipeline, path: &str, marker: &str) { |
| 290 | let path = args.trim(); | 1105 | let h = match rsnix::fs::resolve(path) { |
| 291 | if path.is_empty() { | 1106 | Some(h) => h, |
| 292 | gop_println!("usage: edit <path>"); | 1107 | None => { |
| 293 | serial_println!("usage: edit <path>"); | 1108 | term_err(&alloc::format!("hexdump: {}: no such file", path)); |
| 294 | return; | 1109 | return; |
| 1110 | } | ||
| 1111 | }; | ||
| 1112 | let mut ctx = HexCtx { | ||
| 1113 | pipe, | ||
| 1114 | pos: 0, | ||
| 1115 | row: [0u8; 16], | ||
| 1116 | row_len: 0, | ||
| 1117 | alive: true, | ||
| 1118 | }; | ||
| 1119 | let mut buf = [0u8; 512]; | ||
| 1120 | let mut pos = 0u64; | ||
| 1121 | let mut pending = String::new(); | ||
| 1122 | let mut stopped = false; | ||
| 1123 | 'outer: loop { | ||
| 1124 | if tty::interrupt_pressed() { | ||
| 1125 | break; | ||
| 1126 | } | ||
| 1127 | match rsnix::fs::read(h, &mut buf, pos) { | ||
| 1128 | Ok(0) | Err(_) => break, | ||
| 1129 | Ok(n) => { | ||
| 1130 | pos += n as u64; | ||
| 1131 | pending.push_str(&String::from_utf8_lossy(&buf[..n])); | ||
| 1132 | while let Some(idx) = pending.find('\n') { | ||
| 1133 | let line: String = pending.drain(..=idx).collect(); | ||
| 1134 | let line = line.trim_end_matches('\n'); | ||
| 1135 | if line == marker { | ||
| 1136 | stopped = true; | ||
| 1137 | break 'outer; | ||
| 1138 | } | ||
| 1139 | ctx.feed(line.as_bytes()); | ||
| 1140 | ctx.feed(b"\n"); | ||
| 1141 | if !ctx.alive { | ||
| 1142 | break 'outer; | ||
| 1143 | } | ||
| 1144 | } | ||
| 1145 | } | ||
| 1146 | } | ||
| 295 | } | 1147 | } |
| 296 | if rsnix::fs::ensure_file(path).is_err() { | 1148 | if ctx.alive && !stopped && !pending.is_empty() && pending != marker { |
| 297 | gop_println!("edit: cannot create {}", path); | 1149 | ctx.feed(pending.as_bytes()); |
| 298 | serial_println!("edit: cannot create {}", path); | ||
| 299 | return; | ||
| 300 | } | 1150 | } |
| 301 | rsnix::gop::clear_screen(); | 1151 | ctx.flush(); |
| 302 | rsdosedit::run(path); | ||
| 303 | rsnix::gop::clear_screen(); | ||
| 304 | } | 1152 | } |
| 305 | 1153 | ||
| 306 | fn cmd_mkdir(args: &str) { | 1154 | // --------------------------------------------------------------------------- |
| 307 | let path = args.split_whitespace().next().unwrap_or(""); | 1155 | // Other commands |
| 1156 | // --------------------------------------------------------------------------- | ||
| 1157 | |||
| 1158 | fn cmd_mkdir(p: &Parsed) { | ||
| 1159 | let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); | ||
| 308 | if path.is_empty() { | 1160 | if path.is_empty() { |
| 309 | gop_println!("usage: mkdir <path>"); | 1161 | term_err("usage: mkdir <path>"); |
| 310 | serial_println!("usage: mkdir <path>"); | ||
| 311 | return; | 1162 | return; |
| 312 | } | 1163 | } |
| 313 | match rsnix::fs::mkdir(path) { | 1164 | match rsnix::fs::mkdir(path) { |
| 314 | Ok(()) => {} | 1165 | Ok(()) => {} |
| 315 | Err(()) => { | 1166 | Err(()) => term_err(&alloc::format!("mkdir: cannot create directory '{}'", path)), |
| 316 | gop_println!("mkdir: cannot create directory '{}'", path); | ||
| 317 | serial_println!("mkdir: cannot create directory '{}'", path); | ||
| 318 | } | ||
| 319 | } | 1167 | } |
| 320 | } | 1168 | } |
| 321 | 1169 | ||
| 322 | fn cmd_touch(args: &str) { | 1170 | fn cmd_touch(p: &Parsed) { |
| 323 | let path = args.split_whitespace().next().unwrap_or(""); | 1171 | let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); |
| 324 | if path.is_empty() { | 1172 | if path.is_empty() { |
| 325 | gop_println!("usage: touch <path>"); | 1173 | term_err("usage: touch <path>"); |
| 326 | serial_println!("usage: touch <path>"); | ||
| 327 | return; | 1174 | return; |
| 328 | } | 1175 | } |
| 329 | match rsnix::fs::touch(path) { | 1176 | match rsnix::fs::touch(path) { |
| 330 | Ok(()) => {} | 1177 | Ok(()) => {} |
| 331 | Err(()) => { | 1178 | Err(()) => term_err(&alloc::format!("touch: cannot create '{}'", path)), |
| 332 | gop_println!("touch: cannot create '{}'", path); | ||
| 333 | serial_println!("touch: cannot create '{}'", path); | ||
| 334 | } | ||
| 335 | } | 1179 | } |
| 336 | } | 1180 | } |
| 337 | 1181 | ||
| 338 | fn cmd_rm(args: &str) { | 1182 | fn cmd_rm(p: &Parsed) { |
| 339 | let path = args.split_whitespace().next().unwrap_or(""); | 1183 | let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); |
| 340 | if path.is_empty() { | 1184 | if path.is_empty() { |
| 341 | gop_println!("usage: rm <path>"); | 1185 | term_err("usage: rm <path>"); |
| 342 | serial_println!("usage: rm <path>"); | ||
| 343 | return; | 1186 | return; |
| 344 | } | 1187 | } |
| 345 | match rsnix::fs::remove_path(path) { | 1188 | match rsnix::fs::remove_path(path) { |
| 346 | Ok(()) => {} | 1189 | Ok(()) => {} |
| 347 | Err(()) => { | 1190 | Err(()) => term_err(&alloc::format!( |
| 348 | gop_println!( | 1191 | "rm: cannot remove '{}' (missing, dir not empty, or busy)", |
| 349 | "rm: cannot remove '{}' (missing, dir not empty, or busy)", | 1192 | path |
| 350 | path | 1193 | )), |
| 351 | ); | ||
| 352 | serial_println!( | ||
| 353 | "rm: cannot remove '{}' (missing, dir not empty, or busy)", | ||
| 354 | path | ||
| 355 | ); | ||
| 356 | } | ||
| 357 | } | 1194 | } |
| 358 | } | 1195 | } |
| 359 | 1196 | ||
| 360 | fn cmd_clear() { | 1197 | fn cmd_edit(p: &Parsed) { |
| 1198 | let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); | ||
| 1199 | if path.is_empty() { | ||
| 1200 | term_err("usage: edit <path>"); | ||
| 1201 | return; | ||
| 1202 | } | ||
| 1203 | if rsnix::fs::ensure_file(path).is_err() { | ||
| 1204 | term_err(&alloc::format!("edit: cannot create {}", path)); | ||
| 1205 | return; | ||
| 1206 | } | ||
| 1207 | rsnix::gop::clear_screen(); | ||
| 1208 | rsdosedit::run(path); | ||
| 361 | rsnix::gop::clear_screen(); | 1209 | rsnix::gop::clear_screen(); |
| 362 | } | 1210 | } |
| 363 | 1211 | ||
| 364 | fn cmd_info() { | 1212 | fn cmd_mem(p: &Parsed) { |
| 365 | // Test that the global allocator works: build a String on the heap | ||
| 366 | let mut s = alloc::string::String::from("rs-dos v"); | ||
| 367 | s.push_str(env!("CARGO_PKG_VERSION")); | ||
| 368 | s.push_str(" — rsnix kernel"); | ||
| 369 | gop_println!("{}", s); | ||
| 370 | gop_println!("arch: x86_64, freestanding, no userspace"); | ||
| 371 | serial_println!("{}", s); | ||
| 372 | serial_println!("arch: x86_64, freestanding, no userspace"); | ||
| 373 | // String is freed (dropped) when this scope ends — tests dealloc too | ||
| 374 | } | ||
| 375 | |||
| 376 | fn cmd_mem() { | ||
| 377 | let (map, key) = rsnix::memory::get_map(); | 1213 | let (map, key) = rsnix::memory::get_map(); |
| 378 | 1214 | ||
| 379 | let mut total_kb = 0u64; | 1215 | let mut total_kb = 0u64; |
| @@ -402,98 +1238,76 @@ fn cmd_mem() { | |||
| 402 | } | 1238 | } |
| 403 | } | 1239 | } |
| 404 | 1240 | ||
| 405 | // Build a big heap string with format! to test the allocator | 1241 | let h = rsnix::memory::heap_stats(); |
| 406 | let mut out = alloc::string::String::new(); | 1242 | let ps = rsnix::memory::page_stats(); |
| 407 | use alloc::fmt::Write; | 1243 | |
| 408 | let _ = writeln!(out, "memory map (key=0x{:x}, {} entries)", key, map.len()); | 1244 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 409 | let _ = writeln!( | 1245 | let _ = pipe.line(&alloc::format!("memory map (key=0x{:x}, {} entries)", key, map.len())); |
| 410 | out, | 1246 | let _ = pipe.line(&alloc::format!( |
| 411 | " usable {:4} MB Conventional", | 1247 | " usable {:4} MB Conventional", |
| 412 | conventional_kb / 1024 | 1248 | conventional_kb / 1024 |
| 413 | ); | 1249 | )); |
| 414 | let _ = writeln!( | 1250 | let _ = pipe.line(&alloc::format!( |
| 415 | out, | ||
| 416 | " boot code {:4} MB BootServicesCode", | 1251 | " boot code {:4} MB BootServicesCode", |
| 417 | boot_code_kb / 1024 | 1252 | boot_code_kb / 1024 |
| 418 | ); | 1253 | )); |
| 419 | let _ = writeln!( | 1254 | let _ = pipe.line(&alloc::format!( |
| 420 | out, | ||
| 421 | " boot data {:4} MB BootServicesData", | 1255 | " boot data {:4} MB BootServicesData", |
| 422 | boot_data_kb / 1024 | 1256 | boot_data_kb / 1024 |
| 423 | ); | 1257 | )); |
| 424 | let _ = writeln!( | 1258 | let _ = pipe.line(&alloc::format!( |
| 425 | out, | ||
| 426 | " loader {:4} MB LoaderCode + LoaderData", | 1259 | " loader {:4} MB LoaderCode + LoaderData", |
| 427 | loader_kb / 1024 | 1260 | loader_kb / 1024 |
| 428 | ); | 1261 | )); |
| 429 | let _ = writeln!( | 1262 | let _ = pipe.line(&alloc::format!( |
| 430 | out, | ||
| 431 | " runtime {:4} MB RuntimeServices", | 1263 | " runtime {:4} MB RuntimeServices", |
| 432 | runtime_kb / 1024 | 1264 | runtime_kb / 1024 |
| 433 | ); | 1265 | )); |
| 434 | let _ = writeln!(out, " ACPI {:4} MB", acpi_kb / 1024); | 1266 | let _ = pipe.line(&alloc::format!(" ACPI {:4} MB", acpi_kb / 1024)); |
| 435 | let _ = writeln!(out, " MMIO {:4} MB", mmio_kb / 1024); | 1267 | let _ = pipe.line(&alloc::format!(" MMIO {:4} MB", mmio_kb / 1024)); |
| 436 | let _ = writeln!(out, " reserved {:4} MB", reserved_kb / 1024); | 1268 | let _ = pipe.line(&alloc::format!(" reserved {:4} MB", reserved_kb / 1024)); |
| 437 | let _ = writeln!( | 1269 | let _ = pipe.line(&alloc::format!( |
| 438 | out, | ||
| 439 | " raw total {:4} MB (incl. PCIe MMIO)", | 1270 | " raw total {:4} MB (incl. PCIe MMIO)", |
| 440 | total_kb / 1024 | 1271 | total_kb / 1024 |
| 441 | ); | 1272 | )); |
| 442 | 1273 | let _ = pipe.line("heap:"); | |
| 443 | // Heap (free-list allocator with coalescing) | 1274 | let _ = pipe.line(&alloc::format!( |
| 444 | let h = rsnix::memory::heap_stats(); | ||
| 445 | let _ = writeln!(out, "heap:"); | ||
| 446 | let _ = writeln!( | ||
| 447 | out, | ||
| 448 | " range {:#x}..{:#x} {} KB total", | 1275 | " range {:#x}..{:#x} {} KB total", |
| 449 | h.heap_start, | 1276 | h.heap_start, |
| 450 | h.heap_end, | 1277 | h.heap_end, |
| 451 | h.heap_size / 1024 | 1278 | h.heap_size / 1024 |
| 452 | ); | 1279 | )); |
| 453 | let _ = writeln!( | 1280 | let _ = pipe.line(&alloc::format!( |
| 454 | out, | ||
| 455 | " free {} KB in {} block(s), allocated {} KB", | 1281 | " free {} KB in {} block(s), allocated {} KB", |
| 456 | h.free_bytes / 1024, | 1282 | h.free_bytes / 1024, |
| 457 | h.free_blocks, | 1283 | h.free_blocks, |
| 458 | h.heap_size.saturating_sub(h.free_bytes) / 1024 | 1284 | h.heap_size.saturating_sub(h.free_bytes) / 1024 |
| 459 | ); | 1285 | )); |
| 460 | 1286 | let _ = pipe.line("pages:"); | |
| 461 | // Physical pages (bitmap allocator) | 1287 | let _ = pipe.line(&alloc::format!( |
| 462 | let ps = rsnix::memory::page_stats(); | ||
| 463 | let _ = writeln!(out, "pages:"); | ||
| 464 | let _ = writeln!( | ||
| 465 | out, | ||
| 466 | " span base {:#x}, {} MB covered", | 1288 | " span base {:#x}, {} MB covered", |
| 467 | ps.base_page * rsnix::memory::PAGE_SIZE, | 1289 | ps.base_page * rsnix::memory::PAGE_SIZE, |
| 468 | ps.span_bytes / 1024 / 1024 | 1290 | ps.span_bytes / 1024 / 1024 |
| 469 | ); | 1291 | )); |
| 470 | let _ = writeln!( | 1292 | let _ = pipe.line(&alloc::format!( |
| 471 | out, | ||
| 472 | " free {} pages ({} MB), {} used", | 1293 | " free {} pages ({} MB), {} used", |
| 473 | ps.free_pages, | 1294 | ps.free_pages, |
| 474 | ps.free_pages * rsnix::memory::PAGE_SIZE / 1024 / 1024, | 1295 | ps.free_pages * rsnix::memory::PAGE_SIZE / 1024 / 1024, |
| 475 | ps.used_pages | 1296 | ps.used_pages |
| 476 | ); | 1297 | )); |
| 477 | 1298 | pipe.end(); | |
| 478 | gop_print!("{}", out); | ||
| 479 | serial_print!("{}", out); | ||
| 480 | } | 1299 | } |
| 481 | 1300 | ||
| 482 | // --------------------------------------------------------------------------- | 1301 | // --------------------------------------------------------------------------- |
| 483 | // mmap — dump every entry of the UEFI memory map with type names | 1302 | // mmap — dump every entry of the UEFI memory map with type names |
| 484 | // --------------------------------------------------------------------------- | 1303 | // --------------------------------------------------------------------------- |
| 485 | fn cmd_mmap() { | 1304 | fn cmd_mmap(p: &Parsed) { |
| 486 | let (map, key) = rsnix::memory::get_map(); | 1305 | let (map, key) = rsnix::memory::get_map(); |
| 487 | let mut out = alloc::string::String::new(); | 1306 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 488 | use alloc::fmt::Write; | 1307 | let _ = pipe.line(&alloc::format!("memory map (key=0x{:x}, {} entries):", key, map.len())); |
| 489 | let _ = writeln!(out, "memory map (key=0x{:x}, {} entries):", key, map.len()); | 1308 | let _ = pipe.line(" # type start pages size"); |
| 490 | let _ = writeln!( | ||
| 491 | out, | ||
| 492 | " # type start pages size" | ||
| 493 | ); | ||
| 494 | for (i, d) in map.iter().enumerate() { | 1309 | for (i, d) in map.iter().enumerate() { |
| 495 | let _ = writeln!( | 1310 | let l = alloc::format!( |
| 496 | out, | ||
| 497 | " {:>2} {:<16} {:#014x} {:>9} {:>8} KB", | 1311 | " {:>2} {:<16} {:#014x} {:>9} {:>8} KB", |
| 498 | i, | 1312 | i, |
| 499 | rsnix::uefi::memory_type_name(d.kind), | 1313 | rsnix::uefi::memory_type_name(d.kind), |
| @@ -501,9 +1315,11 @@ fn cmd_mmap() { | |||
| 501 | d.number_of_pages, | 1315 | d.number_of_pages, |
| 502 | d.number_of_pages * 4 | 1316 | d.number_of_pages * 4 |
| 503 | ); | 1317 | ); |
| 1318 | if !pipe.line(&l) { | ||
| 1319 | break; | ||
| 1320 | } | ||
| 504 | } | 1321 | } |
| 505 | gop_print!("{}", out); | 1322 | pipe.end(); |
| 506 | serial_print!("{}", out); | ||
| 507 | } | 1323 | } |
| 508 | 1324 | ||
| 509 | // --------------------------------------------------------------------------- | 1325 | // --------------------------------------------------------------------------- |
| @@ -511,28 +1327,24 @@ fn cmd_mmap() { | |||
| 511 | // Pushes the heap past its base region so growth into the page allocator | 1327 | // Pushes the heap past its base region so growth into the page allocator |
| 512 | // is exercised; also verifies a clean return to the free-list after. | 1328 | // is exercised; also verifies a clean return to the free-list after. |
| 513 | // --------------------------------------------------------------------------- | 1329 | // --------------------------------------------------------------------------- |
| 514 | fn cmd_stress(args: &str) { | 1330 | fn cmd_stress(p: &Parsed) { |
| 515 | let mut it = args.split_whitespace(); | 1331 | let mut it = p.args.iter(); |
| 516 | let chunks: usize = match it.next().and_then(|s| s.parse().ok()) { | 1332 | let chunks: usize = match it.next().and_then(|s| s.parse().ok()) { |
| 517 | Some(n) if (1..=64).contains(&n) => n, | 1333 | Some(n) if (1..=64).contains(&n) => n, |
| 518 | _ => { | 1334 | _ => { |
| 519 | gop_println!("usage: stress <chunks(1-64)> <kb>"); | 1335 | term_err("usage: stress <chunks(1-64)> <kb>"); |
| 520 | serial_println!("usage: stress <chunks(1-64)> <kb>"); | ||
| 521 | return; | 1336 | return; |
| 522 | } | 1337 | } |
| 523 | }; | 1338 | }; |
| 524 | let kb: usize = match it.next().and_then(|s| s.parse().ok()) { | 1339 | let kb: usize = match it.next().and_then(|s| s.parse().ok()) { |
| 525 | Some(n) if n > 0 && n <= 1024 * 1024 => n, | 1340 | Some(n) if n > 0 && n <= 1024 * 1024 => n, |
| 526 | _ => { | 1341 | _ => { |
| 527 | gop_println!("usage: stress <chunks> <kb>"); | 1342 | term_err("usage: stress <chunks> <kb>"); |
| 528 | serial_println!("usage: stress <chunks> <kb>"); | ||
| 529 | return; | 1343 | return; |
| 530 | } | 1344 | } |
| 531 | }; | 1345 | }; |
| 532 | 1346 | ||
| 533 | let mut out = alloc::string::String::new(); | 1347 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 534 | use alloc::fmt::Write; | ||
| 535 | |||
| 536 | let mut allocated = 0usize; | 1348 | let mut allocated = 0usize; |
| 537 | { | 1349 | { |
| 538 | let mut blocks: [Option<alloc::vec::Vec<u8>>; 64] = core::array::from_fn(|_| None); | 1350 | let mut blocks: [Option<alloc::vec::Vec<u8>>; 64] = core::array::from_fn(|_| None); |
| @@ -545,39 +1357,34 @@ fn cmd_stress(args: &str) { | |||
| 545 | allocated += 1; | 1357 | allocated += 1; |
| 546 | } | 1358 | } |
| 547 | Err(_) => { | 1359 | Err(_) => { |
| 548 | let _ = writeln!(out, " block {}: out of memory, stopping", i); | 1360 | let _ = pipe.line(&alloc::format!(" block {}: out of memory, stopping", i)); |
| 549 | break; | 1361 | break; |
| 550 | } | 1362 | } |
| 551 | } | 1363 | } |
| 552 | } | 1364 | } |
| 553 | let h = rsnix::memory::heap_stats(); | 1365 | let h = rsnix::memory::heap_stats(); |
| 554 | let ps = rsnix::memory::page_stats(); | 1366 | let ps = rsnix::memory::page_stats(); |
| 555 | let _ = writeln!( | 1367 | let _ = pipe.line(&alloc::format!( |
| 556 | out, | ||
| 557 | " held {} x {} KB = {} KB", | 1368 | " held {} x {} KB = {} KB", |
| 558 | allocated, | 1369 | allocated, |
| 559 | kb, | 1370 | kb, |
| 560 | allocated * kb | 1371 | allocated * kb |
| 561 | ); | 1372 | )); |
| 562 | let _ = writeln!( | 1373 | let _ = pipe.line(&alloc::format!( |
| 563 | out, | ||
| 564 | " heap free {} KB ({} blocks), pages free {} MB", | 1374 | " heap free {} KB ({} blocks), pages free {} MB", |
| 565 | h.free_bytes / 1024, | 1375 | h.free_bytes / 1024, |
| 566 | h.free_blocks, | 1376 | h.free_blocks, |
| 567 | ps.free_pages * 4 / 1024 | 1377 | ps.free_pages * 4 / 1024 |
| 568 | ); | 1378 | )); |
| 569 | // blocks dropped here → everything freed | 1379 | // blocks dropped here → everything freed |
| 570 | } | 1380 | } |
| 571 | let h = rsnix::memory::heap_stats(); | 1381 | let h = rsnix::memory::heap_stats(); |
| 572 | let _ = writeln!( | 1382 | let _ = pipe.line(&alloc::format!( |
| 573 | out, | ||
| 574 | " freed: heap free {} KB in {} blocks", | 1383 | " freed: heap free {} KB in {} blocks", |
| 575 | h.free_bytes / 1024, | 1384 | h.free_bytes / 1024, |
| 576 | h.free_blocks | 1385 | h.free_blocks |
| 577 | ); | 1386 | )); |
| 578 | 1387 | pipe.end(); | |
| 579 | gop_print!("{}", out); | ||
| 580 | serial_print!("{}", out); | ||
| 581 | } | 1388 | } |
| 582 | 1389 | ||
| 583 | fn cmd_halt() -> ! { | 1390 | fn cmd_halt() -> ! { |
| @@ -586,8 +1393,9 @@ fn cmd_halt() -> ! { | |||
| 586 | rsnix::power::halt() | 1393 | rsnix::power::halt() |
| 587 | } | 1394 | } |
| 588 | 1395 | ||
| 589 | fn cmd_date() { | 1396 | fn cmd_date(p: &Parsed) { |
| 590 | let t = rsnix::rtc::now(); | 1397 | let t = rsnix::rtc::now(); |
| 1398 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 591 | let line = alloc::format!( | 1399 | let line = alloc::format!( |
| 592 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", | 1400 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", |
| 593 | t.year, | 1401 | t.year, |
| @@ -597,19 +1405,17 @@ fn cmd_date() { | |||
| 597 | t.minute, | 1405 | t.minute, |
| 598 | t.second | 1406 | t.second |
| 599 | ); | 1407 | ); |
| 600 | gop_println!("{}", line); | 1408 | let _ = pipe.line(&line); |
| 601 | serial_println!("{}", line); | 1409 | pipe.end(); |
| 602 | } | 1410 | } |
| 603 | 1411 | ||
| 604 | fn cmd_pci() { | 1412 | fn cmd_pci(p: &Parsed) { |
| 605 | let devices = rsnix::pci::probe(); | 1413 | let devices = rsnix::pci::probe(); |
| 606 | let mut out = alloc::string::String::new(); | 1414 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 607 | use alloc::fmt::Write; | 1415 | let _ = pipe.line(&alloc::format!("pci: {} device(s)", devices.len())); |
| 608 | let _ = writeln!(out, "pci: {} device(s)", devices.len()); | 1416 | let _ = pipe.line(" bus dev fn class vendor:device"); |
| 609 | let _ = writeln!(out, " bus dev fn class vendor:device"); | ||
| 610 | for d in &devices { | 1417 | for d in &devices { |
| 611 | let _ = writeln!( | 1418 | let l = alloc::format!( |
| 612 | out, | ||
| 613 | " {:02x}:{:02x}.{} {:<16} {:04x}:{:04x}", | 1419 | " {:02x}:{:02x}.{} {:<16} {:04x}:{:04x}", |
| 614 | d.bus, | 1420 | d.bus, |
| 615 | d.device, | 1421 | d.device, |
| @@ -618,9 +1424,11 @@ fn cmd_pci() { | |||
| 618 | d.vendor, | 1424 | d.vendor, |
| 619 | d.device_id | 1425 | d.device_id |
| 620 | ); | 1426 | ); |
| 1427 | if !pipe.line(&l) { | ||
| 1428 | break; | ||
| 1429 | } | ||
| 621 | } | 1430 | } |
| 622 | gop_print!("{}", out); | 1431 | pipe.end(); |
| 623 | serial_print!("{}", out); | ||
| 624 | } | 1432 | } |
| 625 | 1433 | ||
| 626 | fn cmd_poweroff() -> ! { | 1434 | fn cmd_poweroff() -> ! { |
