diff options
Diffstat (limited to 'shell')
| -rw-r--r-- | shell/src/lib.rs | 133 |
1 files changed, 108 insertions, 25 deletions
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index bc43faf..cf46cb0 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -52,7 +52,7 @@ pub fn run() -> ! { | |||
| 52 | // Read one line from keyboard (tty canonical mode) | 52 | // Read one line from keyboard (tty canonical mode) |
| 53 | match tty::read_line() { | 53 | match tty::read_line() { |
| 54 | LineInput::Interrupted => continue, // ^C — fresh prompt | 54 | LineInput::Interrupted => continue, // ^C — fresh prompt |
| 55 | LineInput::Eof => continue, // ^D on an empty line — fresh prompt (no poweroff) | 55 | LineInput::Eof => continue, // ^D on an empty line — fresh prompt (no poweroff) |
| 56 | LineInput::Line(line) => { | 56 | LineInput::Line(line) => { |
| 57 | let trimmed = line.trim(); | 57 | let trimmed = line.trim(); |
| 58 | if trimmed.is_empty() { | 58 | if trimmed.is_empty() { |
| @@ -132,7 +132,10 @@ fn tokenize(s: &str) -> Result<Vec<Token>, &'static str> { | |||
| 132 | } | 132 | } |
| 133 | 133 | ||
| 134 | fn is_keyword(s: &str) -> bool { | 134 | fn is_keyword(s: &str) -> bool { |
| 135 | matches!(s, "WRITING" | "WRITE" | "TO" | "USE" | "USING" | "TIL" | "UNTIL") | 135 | matches!( |
| 136 | s, | ||
| 137 | "WRITING" | "WRITE" | "TO" | "USE" | "USING" | "TIL" | "UNTIL" | ||
| 138 | ) | ||
| 136 | } | 139 | } |
| 137 | 140 | ||
| 138 | /// How the command's input is fed. Only `cat`/`hexdump`, and only when | 141 | /// How the command's input is fed. Only `cat`/`hexdump`, and only when |
| @@ -189,7 +192,7 @@ fn parse(s: &str) -> Result<Parsed, String> { | |||
| 189 | 192 | ||
| 190 | let mut i = 1; | 193 | let mut i = 1; |
| 191 | let mut args: Vec<String> = Vec::new(); | 194 | let mut args: Vec<String> = Vec::new(); |
| 192 | 195 | ||
| 193 | let mut filters: Vec<Filter> = Vec::new(); | 196 | let mut filters: Vec<Filter> = Vec::new(); |
| 194 | let mut output: Option<Sink> = None; | 197 | let mut output: Option<Sink> = None; |
| 195 | let mut trailing = false; // a filter flag or output keyword has been seen | 198 | let mut trailing = false; // a filter flag or output keyword has been seen |
| @@ -204,6 +207,9 @@ fn parse(s: &str) -> Result<Parsed, String> { | |||
| 204 | if !t.literal { | 207 | if !t.literal { |
| 205 | match text { | 208 | match text { |
| 206 | "USE" | "USING" => { | 209 | "USE" | "USING" => { |
| 210 | if output.is_some() { | ||
| 211 | return Err("USE must come before the output keyword".into()); | ||
| 212 | } | ||
| 207 | if trailing { | 213 | if trailing { |
| 208 | return Err("USE must come before the filters".into()); | 214 | return Err("USE must come before the filters".into()); |
| 209 | } | 215 | } |
| @@ -220,6 +226,9 @@ fn parse(s: &str) -> Result<Parsed, String> { | |||
| 220 | continue; | 226 | continue; |
| 221 | } | 227 | } |
| 222 | "TIL" | "UNTIL" => { | 228 | "TIL" | "UNTIL" => { |
| 229 | if output.is_some() { | ||
| 230 | return Err("TIL must come before the output keyword".into()); | ||
| 231 | } | ||
| 223 | if trailing { | 232 | if trailing { |
| 224 | return Err("TIL must come before the filters".into()); | 233 | return Err("TIL must come before the filters".into()); |
| 225 | } | 234 | } |
| @@ -255,6 +264,9 @@ fn parse(s: &str) -> Result<Parsed, String> { | |||
| 255 | continue; | 264 | continue; |
| 256 | } | 265 | } |
| 257 | "-h" | "-t" => { | 266 | "-h" | "-t" => { |
| 267 | if output.is_some() { | ||
| 268 | return Err("filters must come before the output keyword".into()); | ||
| 269 | } | ||
| 258 | trailing = true; | 270 | trailing = true; |
| 259 | i += 1; | 271 | i += 1; |
| 260 | let n = toks.get(i).ok_or("flag needs a count")?; | 272 | let n = toks.get(i).ok_or("flag needs a count")?; |
| @@ -271,6 +283,9 @@ fn parse(s: &str) -> Result<Parsed, String> { | |||
| 271 | continue; | 283 | continue; |
| 272 | } | 284 | } |
| 273 | "-g" => { | 285 | "-g" => { |
| 286 | if output.is_some() { | ||
| 287 | return Err("filters must come before the output keyword".into()); | ||
| 288 | } | ||
| 274 | trailing = true; | 289 | trailing = true; |
| 275 | i += 1; | 290 | i += 1; |
| 276 | let pat = toks.get(i).ok_or("-g needs a pattern")?; | 291 | let pat = toks.get(i).ok_or("-g needs a pattern")?; |
| @@ -279,6 +294,9 @@ fn parse(s: &str) -> Result<Parsed, String> { | |||
| 279 | continue; | 294 | continue; |
| 280 | } | 295 | } |
| 281 | "-p" => { | 296 | "-p" => { |
| 297 | if output.is_some() { | ||
| 298 | return Err("filters must come before the output keyword".into()); | ||
| 299 | } | ||
| 282 | trailing = true; | 300 | trailing = true; |
| 283 | filters.push(Filter::Pause); | 301 | filters.push(Filter::Pause); |
| 284 | i += 1; | 302 | i += 1; |
| @@ -324,7 +342,10 @@ enum StageState { | |||
| 324 | /// Rolling window of the last N lines. | 342 | /// Rolling window of the last N lines. |
| 325 | Tail(VecDeque<String>), | 343 | Tail(VecDeque<String>), |
| 326 | Grep, | 344 | Grep, |
| 327 | Pause { shown: usize, screen: usize }, | 345 | Pause { |
| 346 | shown: usize, | ||
| 347 | screen: usize, | ||
| 348 | }, | ||
| 328 | } | 349 | } |
| 329 | 350 | ||
| 330 | /// Where the filtered line stream ends up. | 351 | /// Where the filtered line stream ends up. |
| @@ -473,10 +494,9 @@ impl Pipeline { | |||
| 473 | unreachable!("stage state does not match filter") | 494 | unreachable!("stage state does not match filter") |
| 474 | }; | 495 | }; |
| 475 | for l in cur.drain(..) { | 496 | for l in cur.drain(..) { |
| 476 | if *shown > 0 && *shown % *screen == 0 | 497 | if *shown > 0 && *shown % *screen == 0 && !pager_wait() { |
| 477 | && !pager_wait() { | 498 | return false; |
| 478 | return false; | 499 | } |
| 479 | } | ||
| 480 | *shown += 1; | 500 | *shown += 1; |
| 481 | out.push(l); | 501 | out.push(l); |
| 482 | } | 502 | } |
| @@ -529,7 +549,10 @@ impl Pipeline { | |||
| 529 | if let OutSink::File { h: None, .. } = &self.sink { | 549 | if let OutSink::File { h: None, .. } = &self.sink { |
| 530 | self.open_sink(); | 550 | self.open_sink(); |
| 531 | } | 551 | } |
| 532 | if let OutSink::File { h: Some(h), pos, .. } = &mut self.sink { | 552 | if let OutSink::File { |
| 553 | h: Some(h), pos, .. | ||
| 554 | } = &mut self.sink | ||
| 555 | { | ||
| 533 | let mut bytes = l.as_bytes().to_vec(); | 556 | let mut bytes = l.as_bytes().to_vec(); |
| 534 | bytes.push(b'\n'); | 557 | bytes.push(b'\n'); |
| 535 | if let Ok(n) = rsnix::fs::write(*h, &bytes, *pos) { | 558 | if let Ok(n) = rsnix::fs::write(*h, &bytes, *pos) { |
| @@ -544,7 +567,9 @@ impl Pipeline { | |||
| 544 | /// the file's current size. | 567 | /// the file's current size. |
| 545 | fn open_sink(&mut self) { | 568 | fn open_sink(&mut self) { |
| 546 | let (path, overwrite) = match &self.sink { | 569 | let (path, overwrite) = match &self.sink { |
| 547 | OutSink::File { path, overwrite, .. } => (path.clone(), *overwrite), | 570 | OutSink::File { |
| 571 | path, overwrite, .. | ||
| 572 | } => (path.clone(), *overwrite), | ||
| 548 | _ => return, | 573 | _ => return, |
| 549 | }; | 574 | }; |
| 550 | match rsnix::fs::ensure_file(&path) { | 575 | match rsnix::fs::ensure_file(&path) { |
| @@ -616,9 +641,16 @@ fn execute(s: &str) { | |||
| 616 | term_err(&alloc::format!("{}: does not accept input", cmd)); | 641 | term_err(&alloc::format!("{}: does not accept input", cmd)); |
| 617 | return; | 642 | return; |
| 618 | } | 643 | } |
| 619 | if !parsed.args.is_empty() { | 644 | // `cat <path> TIL <marker>` is sugar for `cat USE <path> TIL |
| 645 | // <marker>`: the paths are the input source, truncated at the | ||
| 646 | // marker. An explicit source keyword (USE / USE..TIL) alongside | ||
| 647 | // path arguments is genuinely two input sources. | ||
| 648 | if !parsed.args.is_empty() && !matches!(parsed.input, Input::Til(_)) { | ||
| 620 | term_err(&alloc::format!( | 649 | term_err(&alloc::format!( |
| 621 | "{}: input keyword with path arguments is ambiguous", | 650 | "{}: input source keyword with path arguments is ambiguous — \ |
| 651 | the paths are the input; drop the keyword, or to truncate them at a marker \ | ||
| 652 | write `{} <path> TIL <marker>`", | ||
| 653 | cmd, | ||
| 622 | cmd | 654 | cmd |
| 623 | )); | 655 | )); |
| 624 | return; | 656 | return; |
| @@ -680,8 +712,10 @@ fn cmd_help(_p: &Parsed) { | |||
| 680 | redirection keywords (uppercase, after args): | 712 | redirection keywords (uppercase, after args): |
| 681 | USE/USING <src> read command input from a file (cat/hexdump only) | 713 | USE/USING <src> read command input from a file (cat/hexdump only) |
| 682 | TIL/UNTIL <marker> read input until an exact marker line (heredoc) | 714 | TIL/UNTIL <marker> read input until an exact marker line (heredoc) |
| 715 | cat <path> TIL <marker> reads the path to the marker | ||
| 683 | WRITING/WRITE <dst> write output to a file (overwrite) | 716 | WRITING/WRITE <dst> write output to a file (overwrite) |
| 684 | TO <dst> append output to a file | 717 | TO <dst> append output to a file |
| 718 | order: USE/TIL first, then the -flags, then WRITING/TO last | ||
| 685 | a file sink replaces the screen; > >> < << are gone (> is plain text) | 719 | a file sink replaces the screen; > >> < << are gone (> is plain text) |
| 686 | 720 | ||
| 687 | post-filter flags (trailing, applied left to right): | 721 | post-filter flags (trailing, applied left to right): |
| @@ -758,6 +792,14 @@ fn cmd_ls(p: &Parsed) { | |||
| 758 | // cat — file and stream consumers | 792 | // cat — file and stream consumers |
| 759 | // --------------------------------------------------------------------------- | 793 | // --------------------------------------------------------------------------- |
| 760 | 794 | ||
| 795 | /// How a file read ended: read to EOF, stopped at the TIL marker, or | ||
| 796 | /// aborted (^C / head kill) — the caller should stop processing paths. | ||
| 797 | enum ReadResult { | ||
| 798 | Done, | ||
| 799 | Marker, | ||
| 800 | Aborted, | ||
| 801 | } | ||
| 802 | |||
| 761 | fn cmd_cat(p: &Parsed) { | 803 | fn cmd_cat(p: &Parsed) { |
| 762 | let mut pipe = Pipeline::new(&p.filters, &p.output); | 804 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 763 | match &p.input { | 805 | match &p.input { |
| @@ -778,7 +820,18 @@ fn cmd_cat(p: &Parsed) { | |||
| 778 | cat_file(&mut pipe, path); | 820 | cat_file(&mut pipe, path); |
| 779 | } | 821 | } |
| 780 | Input::Til(marker) => { | 822 | Input::Til(marker) => { |
| 781 | cat_stdin_until(&mut pipe, marker); | 823 | if p.args.is_empty() { |
| 824 | cat_stdin_until(&mut pipe, marker); | ||
| 825 | } else { | ||
| 826 | // `cat <path> TIL <marker>` — the paths are the input | ||
| 827 | // source, truncated at the first marker line. | ||
| 828 | for path in &p.args { | ||
| 829 | match cat_file_until(&mut pipe, path, marker) { | ||
| 830 | ReadResult::Done => {} | ||
| 831 | ReadResult::Marker | ReadResult::Aborted => break, | ||
| 832 | } | ||
| 833 | } | ||
| 834 | } | ||
| 782 | } | 835 | } |
| 783 | Input::UseTil(path, marker) => { | 836 | Input::UseTil(path, marker) => { |
| 784 | cat_file_until(&mut pipe, path, marker); | 837 | cat_file_until(&mut pipe, path, marker); |
| @@ -796,7 +849,7 @@ fn cat_stdin(pipe: &mut Pipeline) -> bool { | |||
| 796 | } | 849 | } |
| 797 | } | 850 | } |
| 798 | LineInput::Interrupted => return false, // ^C aborts the stream | 851 | LineInput::Interrupted => return false, // ^C aborts the stream |
| 799 | LineInput::Eof => return true, // ^D = EOF | 852 | LineInput::Eof => return true, // ^D = EOF |
| 800 | } | 853 | } |
| 801 | } | 854 | } |
| 802 | } | 855 | } |
| @@ -855,12 +908,12 @@ fn cat_file(pipe: &mut Pipeline, path: &str) -> bool { | |||
| 855 | true | 908 | true |
| 856 | } | 909 | } |
| 857 | 910 | ||
| 858 | fn cat_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> bool { | 911 | fn cat_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> ReadResult { |
| 859 | let h = match rsnix::fs::resolve(path) { | 912 | let h = match rsnix::fs::resolve(path) { |
| 860 | Some(h) => h, | 913 | Some(h) => h, |
| 861 | None => { | 914 | None => { |
| 862 | term_err(&alloc::format!("cat: {}: no such file", path)); | 915 | term_err(&alloc::format!("cat: {}: no such file", path)); |
| 863 | return true; | 916 | return ReadResult::Done; |
| 864 | } | 917 | } |
| 865 | }; | 918 | }; |
| 866 | let mut buf = [0u8; 512]; | 919 | let mut buf = [0u8; 512]; |
| @@ -869,7 +922,7 @@ fn cat_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> bool { | |||
| 869 | let mut stopped = false; | 922 | let mut stopped = false; |
| 870 | 'outer: loop { | 923 | 'outer: loop { |
| 871 | if tty::interrupt_pressed() { | 924 | if tty::interrupt_pressed() { |
| 872 | return false; | 925 | return ReadResult::Aborted; |
| 873 | } | 926 | } |
| 874 | match rsnix::fs::read(h, &mut buf, pos) { | 927 | match rsnix::fs::read(h, &mut buf, pos) { |
| 875 | Ok(0) | Err(_) => break, | 928 | Ok(0) | Err(_) => break, |
| @@ -885,16 +938,20 @@ fn cat_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> bool { | |||
| 885 | break 'outer; | 938 | break 'outer; |
| 886 | } | 939 | } |
| 887 | if !pipe.line(line) { | 940 | if !pipe.line(line) { |
| 888 | return false; | 941 | return ReadResult::Aborted; |
| 889 | } | 942 | } |
| 890 | } | 943 | } |
| 891 | } | 944 | } |
| 892 | } | 945 | } |
| 893 | } | 946 | } |
| 894 | if !stopped && !pending.is_empty() && pending != marker && !pipe.line(&pending) { | 947 | if !stopped && !pending.is_empty() && pending != marker && !pipe.line(&pending) { |
| 895 | return false; | 948 | return ReadResult::Aborted; |
| 949 | } | ||
| 950 | if stopped { | ||
| 951 | ReadResult::Marker | ||
| 952 | } else { | ||
| 953 | ReadResult::Done | ||
| 896 | } | 954 | } |
| 897 | true | ||
| 898 | } | 955 | } |
| 899 | 956 | ||
| 900 | // --------------------------------------------------------------------------- | 957 | // --------------------------------------------------------------------------- |
| @@ -998,7 +1055,18 @@ fn cmd_hexdump(p: &Parsed) { | |||
| 998 | hexdump_file(&mut pipe, path); | 1055 | hexdump_file(&mut pipe, path); |
| 999 | } | 1056 | } |
| 1000 | Input::Til(marker) => { | 1057 | Input::Til(marker) => { |
| 1001 | hexdump_stdin_until(&mut pipe, marker); | 1058 | if p.args.is_empty() { |
| 1059 | hexdump_stdin_until(&mut pipe, marker); | ||
| 1060 | } else { | ||
| 1061 | // `hexdump <path> TIL <marker>` — the paths are the input | ||
| 1062 | // source, truncated at the first marker line. | ||
| 1063 | for path in &p.args { | ||
| 1064 | match hexdump_file_until(&mut pipe, path, marker) { | ||
| 1065 | ReadResult::Done => {} | ||
| 1066 | ReadResult::Marker | ReadResult::Aborted => break, | ||
| 1067 | } | ||
| 1068 | } | ||
| 1069 | } | ||
| 1002 | } | 1070 | } |
| 1003 | Input::UseTil(path, marker) => { | 1071 | Input::UseTil(path, marker) => { |
| 1004 | hexdump_file_until(&mut pipe, path, marker); | 1072 | hexdump_file_until(&mut pipe, path, marker); |
| @@ -1101,12 +1169,12 @@ fn hexdump_stdin_until(pipe: &mut Pipeline, marker: &str) { | |||
| 1101 | ctx.flush(); | 1169 | ctx.flush(); |
| 1102 | } | 1170 | } |
| 1103 | 1171 | ||
| 1104 | fn hexdump_file_until(pipe: &mut Pipeline, path: &str, marker: &str) { | 1172 | fn hexdump_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> ReadResult { |
| 1105 | let h = match rsnix::fs::resolve(path) { | 1173 | let h = match rsnix::fs::resolve(path) { |
| 1106 | Some(h) => h, | 1174 | Some(h) => h, |
| 1107 | None => { | 1175 | None => { |
| 1108 | term_err(&alloc::format!("hexdump: {}: no such file", path)); | 1176 | term_err(&alloc::format!("hexdump: {}: no such file", path)); |
| 1109 | return; | 1177 | return ReadResult::Done; |
| 1110 | } | 1178 | } |
| 1111 | }; | 1179 | }; |
| 1112 | let mut ctx = HexCtx { | 1180 | let mut ctx = HexCtx { |
| @@ -1149,6 +1217,13 @@ fn hexdump_file_until(pipe: &mut Pipeline, path: &str, marker: &str) { | |||
| 1149 | ctx.feed(pending.as_bytes()); | 1217 | ctx.feed(pending.as_bytes()); |
| 1150 | } | 1218 | } |
| 1151 | ctx.flush(); | 1219 | ctx.flush(); |
| 1220 | if !ctx.alive { | ||
| 1221 | ReadResult::Aborted | ||
| 1222 | } else if stopped { | ||
| 1223 | ReadResult::Marker | ||
| 1224 | } else { | ||
| 1225 | ReadResult::Done | ||
| 1226 | } | ||
| 1152 | } | 1227 | } |
| 1153 | 1228 | ||
| 1154 | // --------------------------------------------------------------------------- | 1229 | // --------------------------------------------------------------------------- |
| @@ -1242,7 +1317,11 @@ fn cmd_mem(p: &Parsed) { | |||
| 1242 | let ps = rsnix::memory::page_stats(); | 1317 | let ps = rsnix::memory::page_stats(); |
| 1243 | 1318 | ||
| 1244 | let mut pipe = Pipeline::new(&p.filters, &p.output); | 1319 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 1245 | let _ = pipe.line(&alloc::format!("memory map (key=0x{:x}, {} entries)", key, map.len())); | 1320 | let _ = pipe.line(&alloc::format!( |
| 1321 | "memory map (key=0x{:x}, {} entries)", | ||
| 1322 | key, | ||
| 1323 | map.len() | ||
| 1324 | )); | ||
| 1246 | let _ = pipe.line(&alloc::format!( | 1325 | let _ = pipe.line(&alloc::format!( |
| 1247 | " usable {:4} MB Conventional", | 1326 | " usable {:4} MB Conventional", |
| 1248 | conventional_kb / 1024 | 1327 | conventional_kb / 1024 |
| @@ -1304,7 +1383,11 @@ fn cmd_mem(p: &Parsed) { | |||
| 1304 | fn cmd_mmap(p: &Parsed) { | 1383 | fn cmd_mmap(p: &Parsed) { |
| 1305 | let (map, key) = rsnix::memory::get_map(); | 1384 | let (map, key) = rsnix::memory::get_map(); |
| 1306 | let mut pipe = Pipeline::new(&p.filters, &p.output); | 1385 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
| 1307 | let _ = pipe.line(&alloc::format!("memory map (key=0x{:x}, {} entries):", key, map.len())); | 1386 | let _ = pipe.line(&alloc::format!( |
| 1387 | "memory map (key=0x{:x}, {} entries):", | ||
| 1388 | key, | ||
| 1389 | map.len() | ||
| 1390 | )); | ||
| 1308 | let _ = pipe.line(" # type start pages size"); | 1391 | let _ = pipe.line(" # type start pages size"); |
| 1309 | for (i, d) in map.iter().enumerate() { | 1392 | for (i, d) in map.iter().enumerate() { |
| 1310 | let l = alloc::format!( | 1393 | let l = alloc::format!( |
