1use crate::api::console::Style;
2use crate::api::fs;
3use crate::api::process::ExitCode;
4use crate::api::prompt::Prompt;
5use crate::api::regex::Regex;
6use crate::api::syscall;
7use crate::sys::fs::FileType;
8use crate::{api, sys, usr};
9
10use alloc::collections::btree_map::BTreeMap;
11use alloc::format;
12use alloc::string::{String, ToString};
13use alloc::vec::Vec;
14use core::sync::atomic::{fence, Ordering};
15
16const AUTOCOMPLETE_COMMANDS: [&str; 40] = [
18 "2048", "calc", "chess", "copy", "date", "decode", "delete", "dhcp",
19 "diff", "disk", "edit", "elf", "encode", "env", "goto", "hash", "help",
20 "hex", "host", "http", "httpd", "install", "keyboard", "life", "lisp",
21 "list", "memory", "move", "net", "pci", "quit", "read", "render", "shell",
22 "socket", "tcp", "time", "user", "view", "write",
23];
24
25struct Config {
26 env: BTreeMap<String, String>,
27 aliases: BTreeMap<String, String>,
28}
29
30impl Config {
31 fn new() -> Config {
32 let aliases = BTreeMap::new();
33 let mut env = BTreeMap::new();
34 for (key, val) in sys::process::envs() {
35 env.insert(key, val);
37 }
38 env.insert("DIR".to_string(), sys::process::dir());
39 env.insert("status".to_string(), "0".to_string());
40 Config { env, aliases }
41 }
42}
43
44fn autocomplete_commands() -> Vec<String> {
45 let mut res = Vec::new();
46 for cmd in AUTOCOMPLETE_COMMANDS {
47 res.push(cmd.to_string());
48 }
49 if let Ok(files) = fs::read_dir("/bin") {
50 for file in files {
51 res.push(file.name());
52 }
53 }
54 res
55}
56
57fn shell_completer(line: &str) -> Vec<String> {
58 let mut entries = Vec::new();
59 let mut args = split_args(line);
60 if line.ends_with(' ') {
61 args.push(String::new());
62 }
63 let i = args.len() - 1;
64
65 if i == 0 && !fs::is_absolute_path(&args[i]) {
67 for cmd in autocomplete_commands() {
68 if let Some(entry) = cmd.strip_prefix(&args[i]) {
69 entries.push(entry.into());
70 }
71 }
72 }
73
74 if (i == 0 && fs::is_absolute_path(&args[i])) || i > 0 {
76 let path = fs::realpath(&args[i]);
77 let (dirname, filename) = if path.len() > 1 && path.ends_with('/') {
78 (path.trim_end_matches('/'), "")
80 } else {
81 (fs::dirname(&path), fs::filename(&path))
83 };
84 let sep = if dirname.ends_with('/') { "" } else { "/" };
85 if let Ok(files) = fs::read_dir(dirname) {
86 for file in files {
87 let name = file.name();
88 if name.starts_with(filename) {
89 let end = if file.is_dir() { "/" } else { "" };
90 let entry = format!("{}{}{}{}", dirname, sep, name, end);
91 entries.push(entry[path.len()..].into());
92 }
93 }
94 }
95 }
96
97 entries.sort();
98 entries
99}
100
101pub fn prompt_string(success: bool) -> String {
102 let csi_line1 = Style::color("navy");
103 let csi_line2 = Style::color("purple");
104 let csi_error = Style::color("maroon");
105 let csi_reset = Style::reset();
106
107 let mut current_dir = sys::process::dir();
108 if let Some(home) = sys::process::env("HOME") {
109 if current_dir.starts_with(&home) {
110 let n = home.len();
111 current_dir.replace_range(..n, "~");
112 }
113 }
114 let line1 = format!("{}{}{}", csi_line1, current_dir, csi_reset);
115 let line2 = format!(
116 "{}>{} ",
117 if success { csi_line2 } else { csi_error },
118 csi_reset
119 );
120 format!("{}\n{}", line1, line2)
121}
122
123fn is_globbing(arg: &str) -> bool {
124 let arg: Vec<char> = arg.chars().collect();
125 let n = arg.len();
126 if n == 0 {
127 return false;
128 }
129 if arg[0] == '"' && arg[n - 1] == '"' {
130 return false;
131 }
132 if arg[0] == '\'' && arg[n - 1] == '\'' {
133 return false;
134 }
135 for i in 0..n {
136 if arg[i] == '*' || arg[i] == '?' {
137 return true;
138 }
139 }
140 false
141}
142
143fn glob(arg: &str) -> Vec<String> {
144 let mut matches = Vec::new();
145 if is_globbing(arg) {
146 let (dir, pattern, show_dir) = if arg.contains('/') {
147 let d = fs::dirname(arg).to_string();
148 let n = fs::filename(arg).to_string();
149 (d, n, true)
150 } else {
151 (sys::process::dir(), arg.to_string(), false)
152 };
153 let re = Regex::from_glob(&pattern);
154 let sep = if dir == "/" { "" } else { "/" };
155 if let Ok(files) = fs::read_dir(&dir) {
156 for file in files {
157 let name = file.name();
158 if re.is_match(&name) {
159 if show_dir {
160 matches.push(format!("{}{}{}", dir, sep, name));
161 } else {
162 matches.push(name);
163 }
164 }
165 }
166 }
167 } else {
168 matches.push(arg.to_string());
169 }
170 matches
171}
172
173pub fn parse_str(s: &str) -> String {
174 let mut res = String::new();
175 let mut is_escaped = false;
176 for c in s.chars() {
177 match c {
178 '\\' if !is_escaped => {
179 is_escaped = true;
180 continue;
181 }
182 _ if !is_escaped => res.push(c),
183 '\\' => res.push(c),
184 '"' => res.push(c),
185 'n' => res.push('\n'),
186 'r' => res.push('\r'),
187 't' => res.push('\t'),
188 'b' => res.push('\x08'),
189 'e' => res.push('\x1B'),
190 _ => {}
191 }
192 is_escaped = false;
193 }
194 res
195}
196
197pub fn split_args(cmd: &str) -> Vec<String> {
198 let mut args = Vec::new();
199 let mut i = 0;
200 let mut n = cmd.len();
201 let mut is_quote = false;
202 let mut is_escaped = false;
203
204 for (j, c) in cmd.char_indices() {
205 if c == '#' && !is_quote {
206 n = j; break;
208 } else if c == ' ' && !is_quote {
209 if i != j && !cmd[i..j].trim().is_empty() {
210 if args.is_empty() {
211 args.push(cmd[i..j].to_string()) } else {
213 args.extend(glob(&cmd[i..j])) }
215 }
216 i = j + 1;
217 } else if c == '"' && !is_escaped {
218 is_quote = !is_quote;
219 if !is_quote {
220 args.push(parse_str(&cmd[i..j]));
221 }
222 i = j + 1;
223 }
224 if c == '\\' && !is_escaped {
225 is_escaped = true;
226 } else {
227 is_escaped = false;
228 }
229 }
230
231 if i < n {
232 if is_quote {
233 n -= 1;
234 args.push(cmd[i..n].to_string());
235 } else if args.is_empty() {
236 args.push(cmd[i..n].to_string());
237 } else if !cmd[i..n].trim().is_empty() {
238 args.extend(glob(&cmd[i..n]))
239 }
240 }
241
242 if n == 0 {
243 args.push("".to_string());
244 }
245
246 args.iter().map(|s| tilde_expansion(s)).collect()
247}
248
249fn tilde_expansion(arg: &str) -> String {
251 if let Some(home) = sys::process::env("HOME") {
252 let tilde = "~";
253 if arg == tilde || arg.starts_with("~/") {
254 return arg.replacen(tilde, &home, 1);
255 }
256 }
257 arg.to_string()
258}
259
260fn variables_expansion(cmd: &str, config: &mut Config) -> String {
261 let mut cmd = cmd.to_string();
262
263 cmd = cmd.replace("$?", "$status");
265 cmd = cmd.replace("$*", "$1 $2 $3 $4 $5 $6 $7 $8 $9");
266
267 let re = Regex::new("\\$\\w+");
270 while let Some((a, b)) = re.find(&cmd) {
271 let key: String = cmd.chars().skip(a + 1).take(b - a - 1).collect();
272 let val = config.env.get(&key).map_or("", String::as_str);
273 cmd = cmd.replace(&format!("${}", key), val);
274 }
275
276 cmd
277}
278
279fn cmd_change_dir(args: &[&str], config: &mut Config) -> Result<(), ExitCode> {
280 match args.len() {
281 1 => {
282 println!("{}", sys::process::dir());
283 Ok(())
284 }
285 2 => {
286 let mut path = fs::realpath(args[1]);
287 if path.len() > 1 {
288 path = path.trim_end_matches('/').into();
289 }
290 if api::fs::is_dir(&path) {
291 sys::process::set_dir(&path);
292 config.env.insert("DIR".to_string(), sys::process::dir());
293 Ok(())
294 } else {
295 error!("Could not find file '{}'", path);
296 Err(ExitCode::Failure)
297 }
298 }
299 _ => Err(ExitCode::Failure),
300 }
301}
302
303fn cmd_alias(args: &[&str], config: &mut Config) -> Result<(), ExitCode> {
304 if args.len() != 3 {
305 let csi_option = Style::color("aqua");
306 let csi_title = Style::color("yellow");
307 let csi_reset = Style::reset();
308 eprintln!(
309 "{}Usage:{} alias {}<key> <val>{1}",
310 csi_title, csi_reset, csi_option
311 );
312 return Err(ExitCode::UsageError);
313 }
314 config.aliases.insert(args[1].to_string(), args[2].to_string());
315 Ok(())
316}
317
318fn cmd_unalias(args: &[&str], config: &mut Config) -> Result<(), ExitCode> {
319 if args.len() != 2 {
320 let csi_option = Style::color("aqua");
321 let csi_title = Style::color("yellow");
322 let csi_reset = Style::reset();
323 eprintln!(
324 "{}Usage:{} unalias {}<key>{1}",
325 csi_title, csi_reset, csi_option
326 );
327 return Err(ExitCode::UsageError);
328 }
329
330 if config.aliases.remove(args[1]).is_none() {
331 error!("Could not unalias '{}'", args[1]);
332 return Err(ExitCode::Failure);
333 }
334
335 Ok(())
336}
337
338fn cmd_set(args: &[&str], config: &mut Config) -> Result<(), ExitCode> {
339 if args.len() != 3 {
340 let csi_option = Style::color("aqua");
341 let csi_title = Style::color("yellow");
342 let csi_reset = Style::reset();
343 eprintln!(
344 "{}Usage:{} set {}<key> <val>{1}",
345 csi_title, csi_reset, csi_option
346 );
347 return Err(ExitCode::UsageError);
348 }
349
350 config.env.insert(args[1].to_string(), args[2].to_string());
351 Ok(())
352}
353
354fn cmd_unset(args: &[&str], config: &mut Config) -> Result<(), ExitCode> {
355 if args.len() != 2 {
356 let csi_option = Style::color("aqua");
357 let csi_title = Style::color("yellow");
358 let csi_reset = Style::reset();
359 eprintln!(
360 "{}Usage:{} unset {}<key>{1}",
361 csi_title, csi_reset, csi_option
362 );
363 return Err(ExitCode::UsageError);
364 }
365
366 if config.env.remove(args[1]).is_none() {
367 error!("Could not unset '{}'", args[1]);
368 return Err(ExitCode::Failure);
369 }
370
371 Ok(())
372}
373
374fn cmd_logs() -> Result<(), ExitCode> {
375 print!("{}", sys::log::read());
376 Ok(())
377}
378
379fn cmd_version() -> Result<(), ExitCode> {
380 println!(
381 "MOROS v{}",
382 option_env!("MOROS_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
383 );
384 Ok(())
385}
386
387fn exec_with_config(cmd: &str, config: &mut Config) -> Result<(), ExitCode> {
388 let cmd = variables_expansion(cmd, config);
389 let mut args = split_args(cmd.trim());
390 if args.is_empty() {
391 return Ok(());
392 }
393
394 if let Some(alias) = config.aliases.get(&args[0]) {
396 args.remove(0);
397 for arg in alias.split(' ').rev() {
398 args.insert(0, arg.to_string())
399 }
400 }
401
402 let mut args: Vec<&str> = args.iter().map(String::as_str).collect();
403
404 let mut restore_handles = false;
406 let mut n = args.len();
407 let mut i = 0;
408 loop {
409 if i == n {
410 break;
411 }
412
413 let mut is_fat_arrow = false;
414 let mut is_thin_arrow = false;
415 let mut head_count = 0;
416 let mut left_handle;
417 if Regex::new("^[?\\d*]?-+>$").is_match(args[i]) {
418 is_thin_arrow = true;
423 left_handle = 1;
424 } else if Regex::new("^<=*>+$").is_match(args[i]) {
425 is_fat_arrow = true;
426 left_handle = 0;
427 n += 2;
428 args.insert(i + 2, args[i + 1]);
429 args.insert(i + 2, args[i].trim_start_matches('<'));
430 args[i] = "<=";
431 } else if Regex::new("^[?\\d*]?=*>+[?\\d*]?$").is_match(args[i]) {
432 is_fat_arrow = true;
439 left_handle = 1;
440 } else if Regex::new("^<=*$").is_match(args[i]) {
441 is_fat_arrow = true;
446 left_handle = 0;
447 } else {
448 i += 1;
449 continue;
450 }
451
452 let mut num = String::new();
454 for c in args[i].chars() {
455 match c {
456 '[' | ']' | '-' | '=' => {
457 continue;
458 }
459 '<' | '>' => {
460 head_count += 1;
461 if let Ok(handle) = num.parse() {
462 left_handle = handle;
463 }
464 num.clear();
465 }
466 _ => {
467 num.push(c);
468 }
469 }
470 }
471
472 if is_fat_arrow {
473 restore_handles = true;
475 if !num.is_empty() {
476 error!("Redirecting to a handle has not been implemented yet");
478 return Err(ExitCode::Failure);
479 } else {
480 if i == n - 1 {
481 error!("Could not parse path for redirection");
482 return Err(ExitCode::Failure);
483 }
484 let path = args[i + 1];
485 let append_mode = head_count > 1;
486 if api::fs::reopen(path, left_handle, append_mode).is_err() {
487 error!("Could not open path for redirection");
488 return Err(ExitCode::Failure);
489 }
490 args.remove(i); n -= 1;
492 }
493 n -= 1;
494 args.remove(i); } else if is_thin_arrow {
496 error!("Piping has not been implemented yet");
497 return Err(ExitCode::Failure);
498 }
499 }
500
501 fence(Ordering::SeqCst);
502 let res = dispatch(&args, config);
503
504 if restore_handles {
506 for i in 0..3 {
507 api::fs::reopen("/dev/console", i, false).ok();
508 }
509 }
510
511 res
512}
513
514fn dispatch(args: &[&str], config: &mut Config) -> Result<(), ExitCode> {
515 match args[0] {
516 "" => Ok(()),
517 "2048" => usr::pow::main(args),
518 "alias" => cmd_alias(args, config),
519 "calc" => usr::calc::main(args),
521 "chess" => usr::chess::main(args),
522 "copy" => usr::copy::main(args),
523 "date" => usr::date::main(args),
524 "decode" => usr::decode::main(args),
525 "delete" => usr::delete::main(args),
526 "dhcp" => usr::dhcp::main(args),
527 "diff" => usr::diff::main(args),
528 "disk" => usr::disk::main(args),
529 "edit" => usr::edit::main(args),
530 "elf" => usr::elf::main(args),
531 "encode" => usr::encode::main(args),
532 "env" => usr::env::main(args),
533 "find" => usr::find::main(args),
534 "goto" => cmd_change_dir(args, config), "hash" => usr::hash::main(args),
537 "help" => usr::help::main(args),
538 "hex" => usr::hex::main(args),
539 "host" => usr::host::main(args),
540 "http" => usr::http::main(args),
541 "httpd" => usr::httpd::main(args),
542 "install" => usr::install::main(args),
543 "keyboard" => usr::keyboard::main(args),
544 "life" => usr::life::main(args),
545 "lisp" => usr::lisp::main(args),
546 "list" => usr::list::main(args),
547 "logs" => cmd_logs(),
548 "memory" => usr::memory::main(args),
549 "move" => usr::r#move::main(args),
550 "net" => usr::net::main(args),
551 "pci" => usr::pci::main(args),
552 "pi" => usr::pi::main(args),
553 "quit" => Err(ExitCode::ShellExit),
554 "read" => usr::read::main(args),
555 "render" => usr::render::main(args),
556 "set" => cmd_set(args, config),
557 "shell" => usr::shell::main(args),
558 "socket" => usr::socket::main(args),
559 "tcp" => usr::tcp::main(args),
560 "time" => usr::time::main(args),
561 "unalias" => cmd_unalias(args, config),
562 "unset" => cmd_unset(args, config),
563 "version" => cmd_version(),
564 "user" => usr::user::main(args),
565 "view" => usr::view::main(args),
566 "write" => usr::write::main(args),
567 "panic" => panic!("{}", args[1..].join(" ")),
568 _ => {
569 let mut path = fs::realpath(args[0]);
570 if path.len() > 1 {
571 path = path.trim_end_matches('/').into();
572 }
573 match syscall::info(&path).map(|info| info.kind()) {
574 Some(FileType::Dir) => {
575 sys::process::set_dir(&path);
576 config.env.insert("DIR".to_string(), sys::process::dir());
577 Ok(())
578 }
579 Some(FileType::File) => {
580 spawn(&path, args, config)
581 }
582 _ => {
583 let path = format!("/bin/{}", args[0]);
584 spawn(&path, args, config)
585 }
586 }
587 }
588 }
589}
590
591fn spawn(
592 path: &str,
593 args: &[&str],
594 config: &mut Config
595) -> Result<(), ExitCode> {
596 if let Ok(contents) = fs::read_to_string(path) {
598 if contents.starts_with("#!") {
599 if let Some(line) = contents.lines().next() {
600 let mut new_args = Vec::with_capacity(args.len() + 1);
601 new_args.push(line[2..].trim());
602 new_args.push(path);
603 new_args.extend(&args[1..]);
604 return dispatch(&new_args, config);
605 }
606 }
607 }
608
609 match api::process::spawn(path, args) {
611 Err(ExitCode::ExecError) => {
612 error!("Could not execute '{}'", args[0]);
613 Err(ExitCode::ExecError)
614 }
615 Err(ExitCode::ReadError) => {
616 error!("Could not read '{}'", args[0]);
617 Err(ExitCode::ReadError)
618 }
619 Err(ExitCode::OpenError) => {
620 error!("Could not open '{}'", args[0]);
621 Err(ExitCode::OpenError)
622 }
623 res => res,
624 }
625}
626
627fn repl(config: &mut Config) -> Result<(), ExitCode> {
628 println!();
629
630 let mut prompt = Prompt::new();
631 let history_file = "~/.shell-history";
632 prompt.history.load(history_file);
633 prompt.completion.set(&shell_completer);
634
635 let mut code = ExitCode::Success;
636 let success = code;
637 while let Some(cmd) = prompt.input(&prompt_string(code == success)) {
638 code = match exec_with_config(&cmd, config) {
639 Err(ExitCode::ShellExit) => break,
640 Err(e) => e,
641 Ok(()) => ExitCode::Success,
642 };
643 config.env.insert("status".to_string(), format!("{}", code as u8));
644 prompt.history.add(&cmd);
645 prompt.history.save(history_file);
646 sys::console::drain();
647 println!();
648 }
649 print!("\x1b[2J\x1b[1;1H"); Ok(())
651}
652
653pub fn exec(cmd: &str) -> Result<(), ExitCode> {
654 let mut config = Config::new();
655 exec_with_config(cmd, &mut config)
656}
657
658pub fn main(args: &[&str]) -> Result<(), ExitCode> {
659 let mut config = Config::new();
660
661 if let Ok(contents) = fs::read_to_string("/ini/shell.sh") {
662 for cmd in contents.lines() {
663 exec_with_config(cmd, &mut config).ok();
664 }
665 }
666
667 if args.len() < 2 {
668 config.env.insert(0.to_string(), args[0].to_string());
669
670 repl(&mut config)
671 } else {
672 if args[1] == "-h" || args[1] == "--help" {
673 return help();
674 }
675 config.env.insert(0.to_string(), args[1].to_string());
676
677 for (i, arg) in args[2..].iter().enumerate() {
679 config.env.insert((i + 1).to_string(), arg.to_string());
680 }
681
682 let path = args[1];
683 if let Ok(contents) = api::fs::read_to_string(path) {
684 for line in contents.lines() {
685 if !line.is_empty() {
686 exec_with_config(line, &mut config).ok();
687 }
688 }
689 Ok(())
690 } else {
691 error!("Could not read file '{}'", path);
692 Err(ExitCode::Failure)
693 }
694 }
695}
696
697fn help() -> Result<(), ExitCode> {
698 let csi_option = Style::color("aqua");
699 let csi_title = Style::color("yellow");
700 let csi_reset = Style::reset();
701 println!(
702 "{}Usage:{} shell {}[<file> [<args>]]{}",
703 csi_title, csi_reset, csi_option, csi_reset
704 );
705 Ok(())
706}
707
708#[test_case]
709fn test_shell() {
710 use alloc::string::ToString;
711
712 sys::fs::mount_mem();
713 sys::fs::format_mem();
714 usr::install::copy_files(false);
715
716 exec("print test1 => /tmp/test1").ok();
718 assert_eq!(
719 api::fs::read_to_string("/tmp/test1"),
720 Ok("test1\n".to_string())
721 );
722
723 exec("print test2 1=> /tmp/test2").ok();
725 assert_eq!(
726 api::fs::read_to_string("/tmp/test2"),
727 Ok("test2\n".to_string())
728 );
729
730 exec("hex /nope 2=> /tmp/test3").ok();
732 assert!(api::fs::read_to_string("/tmp/test3").unwrap().
733 contains("Could not read file '/nope'"));
734
735 let mut config = Config::new();
736 exec_with_config("set b 42", &mut config).ok();
737 exec_with_config("print a $b $c d => /test", &mut config).ok();
738 assert_eq!(api::fs::read_to_string("/test"), Ok("a 42 d\n".to_string()));
739
740 sys::fs::dismount();
741}
742
743#[test_case]
744fn test_split_args() {
745 use alloc::vec;
746 assert_eq!(split_args(""), vec![""]);
747 assert_eq!(split_args("print"), vec!["print"]);
748 assert_eq!(split_args("print "), vec!["print"]);
749 assert_eq!(split_args("print "), vec!["print"]);
750 assert_eq!(split_args("print # comment"), vec!["print"]);
751 assert_eq!(split_args("print foo"), vec!["print", "foo"]);
752 assert_eq!(split_args("print foo "), vec!["print", "foo"]);
753 assert_eq!(split_args("print foo "), vec!["print", "foo"]);
754 assert_eq!(split_args("print foo # comment"), vec!["print", "foo"]);
755 assert_eq!(split_args("print foo bar"), vec!["print", "foo", "bar"]);
756 assert_eq!(split_args("print foo bar"), vec!["print", "foo", "bar"]);
757 assert_eq!(split_args("print foo bar"), vec!["print", "foo", "bar"]);
758 assert_eq!(split_args("print foo \"bar\""), vec!["print", "foo", "bar"]);
759 assert_eq!(split_args("print foo \"\""), vec!["print", "foo", ""]);
760 assert_eq!(
761 split_args("print foo \"bar\" "),
762 vec!["print", "foo", "bar"]
763 );
764 assert_eq!(split_args("print foo \"\" "), vec!["print", "foo", ""]);
765}
766
767#[test_case]
768fn test_variables_expansion() {
769 let mut config = Config::new();
770 exec_with_config("set foo 42", &mut config).ok();
771 exec_with_config("set bar \"Alice and Bob\"", &mut config).ok();
772 assert_eq!(variables_expansion("print $foo", &mut config), "print 42");
773 assert_eq!(
774 variables_expansion("print \"Hello $bar\"", &mut config),
775 "print \"Hello Alice and Bob\""
776 );
777}