summaryrefslogtreecommitdiffstats
path: root/shell
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 13:14:07 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 13:14:07 +0200
commit6eb14b64209b9d24554c587f610d7e300ef809ea (patch)
treec308507a2140680eb82fdb8b360aa346e7c5c9fe /shell
parentBump workspace to edition 2024 (diff)
downloadrs-dos-6eb14b64209b9d24554c587f610d7e300ef809ea.tar.gz
rs-dos-6eb14b64209b9d24554c587f610d7e300ef809ea.zip
shell: keyword redirection + post-filter flags (ADR 0002)
Implement the shell experience redesign: one canonical grammar for every command — 'cmd args [USE src] [TIL marker] [filters...] [output kw dst]'. - Keyword redirection (uppercase-only, aliases valid, fully reserved): WRITING/WRITE overwrite, TO append, USE/USING input source, TIL/UNTIL heredoc/terminator (exact full-line marker). > >> < << are removed; '>' is plain text. - Post-filter stages applied left to right over a line stream, each repeatable: -h N (head kills the upstream), -t N (tail holds a rolling window, flushes at EOF/^C), -g PAT (smart-case literal substring), -p (screenful pager, terminal only). A file sink replaces the screen; -p is a no-op with one. - C-style quoting: "..." groups and literalizes, \x escapes one char; quoted/escaped text is never an operator, never split. - Input consumers: cat/hexdump, path-less only (stdin until ^D, USE f, TIL m heredoc, USE f TIL m truncation). Other commands reject input keywords; paths + input keyword is a syntax error. - ^D is EOF everywhere and never powers off; poweroff/exit are the only shutdown verbs. - help rewritten; test-disk.sh and sendkeys.py use the keyword syntax. Verified in QEMU: 26 grammar checks on fresh and reused disks, disk round trip, pager pacing, -h stopping /dev/mem at N lines, ^C flushing a -t window. Two bugs found during verification and fixed: 1. WRITING over an existing file wrote after stale NUL padding — fs::size() is stale after set_len(0); the sink writes at offset 0 after truncation. 2. USE f TIL m leaked the post-marker remainder — the final partial-line emit ran after the marker break; guarded with a stopped flag.
Diffstat (limited to '')
-rw-r--r--shell/src/lib.rs1458
1 files changed, 1133 insertions, 325 deletions
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
3extern 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
5use alloc::string::ToString; 25extern crate alloc;
6 26
7/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. 27use alloc::collections::VecDeque;
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
8use rsnix::tty::{self, LineInput}; 30use rsnix::tty::{self, LineInput};
9use rsnix::{gop_print, gop_println, serial_print, serial_println}; 31use 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
72fn 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).
83struct 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(), 91fn 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
134fn 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)]
141enum 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
151impl 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)]
159enum Filter {
160 Head(usize),
161 Tail(usize),
162 Grep(String),
163 Pause,
164}
165
166/// Output destination.
167#[derive(Clone)]
168enum Sink {
169 Write(String),
170 Append(String),
171}
172
173struct 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.
183fn 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.
84fn split_first_word(s: &str) -> (&str, &str) { 321enum 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.
331enum 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.
343fn 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 354fn 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
97fn 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"); 374struct 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
126fn cmd_echo(args: &str) { 379impl 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).
575fn 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`).
600fn accepts_input(cmd: &str) -> bool {
601 matches!(cmd, "cat" | "hexdump")
602}
603
604fn 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
656fn 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
680redirection 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
687post-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
693quoting: \"...\" groups text; \\x escapes one char. Quoted text is never
694an 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
700fn 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
707fn cmd_hello(p: &Parsed) {
708 let mut pipe = Pipeline::new(&p.filters, &p.output);
709 let _ = pipe.line("World!");
710 pipe.end();
711}
712
713fn cmd_clear() {
714 rsnix::gop::clear_screen();
715}
716
717fn 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
157fn cmd_ls(args: &str) { 727fn 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
761fn 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
189fn cmd_cat(args: &str) { 790fn 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
804fn 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
822fn 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
858fn 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
227fn 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>"); 904struct 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
912fn 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
931fn 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
954impl 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
983fn 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
1010fn 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
1047fn 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// --------------------------------------------------------------------------- 1074fn 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
289fn cmd_edit(args: &str) { 1104fn 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
306fn cmd_mkdir(args: &str) { 1154// ---------------------------------------------------------------------------
307 let path = args.split_whitespace().next().unwrap_or(""); 1155// Other commands
1156// ---------------------------------------------------------------------------
1157
1158fn 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
322fn cmd_touch(args: &str) { 1170fn 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
338fn cmd_rm(args: &str) { 1182fn 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
360fn cmd_clear() { 1197fn 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
364fn cmd_info() { 1212fn 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
376fn 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// ---------------------------------------------------------------------------
485fn cmd_mmap() { 1304fn 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// ---------------------------------------------------------------------------
514fn cmd_stress(args: &str) { 1330fn 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
583fn cmd_halt() -> ! { 1390fn cmd_halt() -> ! {
@@ -586,8 +1393,9 @@ fn cmd_halt() -> ! {
586 rsnix::power::halt() 1393 rsnix::power::halt()
587} 1394}
588 1395
589fn cmd_date() { 1396fn 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
604fn cmd_pci() { 1412fn 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
626fn cmd_poweroff() -> ! { 1434fn cmd_poweroff() -> ! {