moros/usr/
shell.rs

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
16// TODO: Scan /bin
17const 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            // Copy the process environment to the shell environment
36            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    // Autocomplete command
66    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    // Autocomplete path
75    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            // List files in dir (/path/to/ -> /path/to/file.txt)
79            (path.trim_end_matches('/'), "")
80        } else {
81            // List matching files (/path/to/fi -> /path/to/file.txt)
82            (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; // Discard comments
207            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()) // program name
212                } else {
213                    args.extend(glob(&cmd[i..j])) // program args
214                }
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
249// Replace `~` with the value of `$HOME` when it's at the begining of an arg
250fn 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    // Special cases for none alphanum (\w) variables
264    cmd = cmd.replace("$?", "$status");
265    cmd = cmd.replace("$*", "$1 $2 $3 $4 $5 $6 $7 $8 $9");
266
267    // Replace alphanum `$key` with its value in the environment
268    // or an empty string.
269    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    // Replace command alias
395    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    // Redirections
405    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            // Pipes
419            // read foo.txt --> write bar.txt
420            // read foo.txt -> write bar.txt
421            // read foo.txt [2]-> write /dev/null
422            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            // Redirections to
433            // read foo.txt ==> bar.txt
434            // read foo.txt => bar.txt
435            // read foo.txt > bar.txt
436            // read foo.txt [1]=> /dev/null
437            // read foo.txt [1]=>[3]
438            is_fat_arrow = true;
439            left_handle = 1;
440        } else if Regex::new("^<=*$").is_match(args[i]) {
441            // Redirections from
442            // write bar.txt <== foo.txt
443            // write bar.txt <= foo.txt
444            // write bar.txt < foo.txt
445            is_fat_arrow = true;
446            left_handle = 0;
447        } else {
448            i += 1;
449            continue;
450        }
451
452        // Parse handles
453        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            // Redirections
474            restore_handles = true;
475            if !num.is_empty() {
476                // if let Ok(right_handle) = num.parse() {}
477                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); // Remove path from args
491                n -= 1;
492            }
493            n -= 1;
494            args.remove(i); // Remove redirection from args
495        } 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    // TODO: Remove this when redirections are done in spawned process
505    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        //"beep"     => usr::beep::main(args),
520        "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        //"geodate"  => usr::geodate::main(args),
535        "goto"     => cmd_change_dir(args, config), // TODO: Remove this
536        "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    // Script
597    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    // Binary
610    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"); // Clear screen and move to top
650    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        // Add script arguments to the environment as `$1`, `$2`, `$3`, ...
678        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    // Redirect standard output
717    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    // Redirect standard output explicitely
724    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    // Redirect standard error explicitely
731    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}