From 555980de8e3e251de73f54bcbd0dfa07d71371a7 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sat, 10 Oct 2020 22:35:49 +0200 Subject: [PATCH 01/14] Add macro debug_println --- src/debug.rs | 7 +++++++ src/lexer.rs | 2 +- src/lib.rs | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/debug.rs diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..b064e26 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,7 @@ +/// Alias of println! but only present in debug builds. +macro_rules! debug_println { + ($($t:tt)*) => { + #[cfg(debug_assertions)] // Only include when not built with `--release` flag + println!($($t)*); + } +} diff --git a/src/lexer.rs b/src/lexer.rs index 4243ab2..c696c52 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -360,7 +360,7 @@ impl Lexer { } Some(_) => match self.read_until(false, false, false, Box::new(is_token_split)) { Ok(w) => { - println!("The words I got: {:?}", w); + debug_println!("The words I got: {:?}", w); match &w[..] { [Literal(s), ..] if s.ends_with('=') diff --git a/src/lib.rs b/src/lib.rs index ee7ee20..1ebc8e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod debug; + pub mod lexer; pub mod parser; pub mod runner; From 3791797181ed24d15b9bc795eeda3bffc3425823 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 20:36:46 +0200 Subject: [PATCH 02/14] Add basic integration tests --- Cargo.lock | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 ++ tests/cli.rs | 54 ++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 tests/cli.rs diff --git a/Cargo.lock b/Cargo.lock index a21f536..468f04a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,18 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "assert_cmd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c88b9ca26f9c16ec830350d309397e74ee9abdfd8eb1f71cb6ecc71a3fc818da" +dependencies = [ + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -18,12 +31,57 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "escargot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74cf96bec282dcdb07099f7e31d9fed323bca9435a09aba7b6d99b7617bca96d" +dependencies = [ + "lazy_static", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + [[package]] name = "nix" version = "0.18.0" @@ -46,12 +104,128 @@ dependencies = [ "winapi", ] +[[package]] +name = "predicates" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bfead12e90dccead362d62bb2c90a5f6fc4584963645bc7f71a735e0b0735a" +dependencies = [ + "difference", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" + +[[package]] +name = "predicates-tree" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" +dependencies = [ + "predicates-core", + "treeline", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rush" version = "0.1.0" dependencies = [ + "assert_cmd", + "escargot", "nix", "os_pipe", + "predicates-core", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2e59c50ed8f6b050b071aa7b6865293957a9af6b58b94f97c1c9434ad440ea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7fec9b1..b7e32b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,8 @@ edition = "2018" [dependencies] os_pipe = "0.9.2" nix = "0.18.0" + +[dev-dependencies] +assert_cmd = "1.0.1" +escargot = "0.5.0" +predicates-core = "1.0.0" diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..1dc7fd7 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,54 @@ +use assert_cmd::assert::IntoOutputPredicate; +use assert_cmd::cmd::Command; +use escargot::CargoBuild; +use predicates_core::Predicate; + +fn test_io( + input: I, + output: O, +) -> Result> +where + I: Into>, + O: IntoOutputPredicate

, + P: Predicate<[u8]>, +{ + let mut cmd = Command::from_std( + CargoBuild::new() + .bin(env!("CARGO_PKG_NAME")) + .release() + .run()? + .command(), + ); + let assert = cmd.write_stdin(input).assert(); + Ok(assert.stdout(output)) +} + +#[test] +fn echo_prints_argument() -> Result<(), Box> { + test_io( + "echo foo", + "$> foo +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn empty_input_prints_newline() -> Result<(), Box> { + test_io( + "", "$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn exit_prints_nothing() -> Result<(), Box> { + test_io("exit", "$> ")?.success().code(0); + Ok(()) +} From a1978be77cf3d0729ce749031887c5a2442c129f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 21:01:17 +0200 Subject: [PATCH 03/14] Test that arguments are not parsed as environment variables --- src/parser.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index 1a8f9cc..5f64724 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -394,6 +394,7 @@ mod parser_tests { use crate::helpers::Shell; use crate::lexer::Lexer; use std::cell::RefCell; + use std::collections::HashMap; use std::rc::Rc; #[test] @@ -455,4 +456,27 @@ mod parser_tests { )); assert_eq!(expected, parser.get().unwrap()) } + + #[test] + fn test_cmd_with_equals_in_args() { + let shell = Rc::new(RefCell::new(Shell::new(None))); + let lexer = Lexer::new("LANG=en alias foo='bar' foo boo=\"far\"", Rc::clone(&shell)); + let mut parser = Parser::new(lexer, Rc::clone(&shell)); + let expected = Cmd::Simple({ + let mut s = Simple::new( + String::from("alias"), + vec![ + "foo=bar".to_string(), + "foo".to_string(), + "boo=far".to_string(), + ], + Io::new(), + ); + let mut env = HashMap::new(); + env.insert("LANG".to_string(), "en".to_string()); + s.add_env(env); + s + }); + assert_eq!(expected, parser.get().unwrap()) + } } From 5c4852878a3dd3040103ad27fcffc8b16846b46e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 21:14:09 +0200 Subject: [PATCH 04/14] Fix arguments being parsed as env variables --- src/parser.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 5f64724..5f78029 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -136,9 +136,33 @@ impl Parser let mut result = Vec::new(); let mut io = Io::new(); let mut map = HashMap::new(); + let mut env_finished = false; loop { + let mut will_finish_env = true; match self.lexer.peek() { + Some(Assign(_, _)) if !env_finished => { + if let Some(Assign(key, var)) = self.lexer.next() { + map.insert(key, self.expand_word(var)); + } + will_finish_env = false; + } + Some(Assign(_, _)) if env_finished => { + // Stick it back together as if it were a plain word + if let Some(Assign(lhs, mut rhs_expansions)) = self.lexer.next() { + if let [Literal(_)] = &rhs_expansions[..] { + result.push(format!( + "{}={}", + lhs, + rhs_expansions.pop().unwrap().get_name() + )) + } else { + let word = self.expand_word(rhs_expansions); + result.push(format!("{}={}", lhs, word)) + } + } + } + Some(Word(_)) => { if let Some(Word(mut expansions)) = self.lexer.next() { if let [Literal(_)] = &expansions[..] { @@ -151,11 +175,6 @@ impl Parser } } } - Some(Assign(_, _)) => { - if let Some(Assign(key, var)) = self.lexer.next() { - map.insert(key, self.expand_word(var)); - } - } Some(Op(Op::Less)) => { self.lexer.next(); io.set_stdin(self.token_to_fd(&io)?); @@ -181,6 +200,7 @@ impl Parser } _ => break, } + env_finished = env_finished || will_finish_env; } if result.is_empty() { if map.is_empty() { From 1bd813a4ea4d0aa301b3f2746796fd5cb10cc037 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 22:30:24 +0200 Subject: [PATCH 05/14] Implement alias builtin --- Cargo.lock | 43 +++++++++++++++++++++++++++++ Cargo.toml | 1 + src/builtins.rs | 32 ++++++++++++++++++++++ src/helpers.rs | 2 ++ src/runner.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++-- tests/cli.rs | 54 +++++++++++++++++++++++++++++++++++++ 6 files changed, 202 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 468f04a..8e571fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" +dependencies = [ + "memchr", +] + [[package]] name = "assert_cmd" version = "1.0.1" @@ -82,6 +91,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + [[package]] name = "nix" version = "0.18.0" @@ -148,6 +163,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c" + [[package]] name = "rush" version = "0.1.0" @@ -157,6 +190,7 @@ dependencies = [ "nix", "os_pipe", "predicates-core", + "regex", ] [[package]] @@ -207,6 +241,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + [[package]] name = "treeline" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b7e32b6..1d027e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] os_pipe = "0.9.2" nix = "0.18.0" +regex = "1.3.9" [dev-dependencies] assert_cmd = "1.0.1" diff --git a/src/builtins.rs b/src/builtins.rs index ac86e6e..9d60616 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1,3 +1,5 @@ +use regex::Regex; +use std::collections::HashMap; use std::process::exit as exit_program; use std::env; use std::rc::Rc; @@ -7,6 +9,36 @@ use crate::helpers::Shell; // Unless specified otherwise, if provided multiple arguments while only // accepting one, these use the first argument. Dash does this as well. +pub fn alias( + // Aliases can be added and then printed in the same command + aliases: &mut HashMap, + args: Vec, +) -> bool { + if args.len() == 0 { + for (lhs, rhs) in aliases { + println!("alias {}='{}'", lhs, rhs); + } + true + } else { + let mut success = true; + let assignment_re = Regex::new(r"^(\w+)=(.*)").unwrap(); + for arg in args { + if assignment_re.is_match(&arg) { + let caps = assignment_re.captures(&arg).unwrap(); + let lhs = &caps[1]; + let rhs = &caps[2]; + aliases.insert(lhs.to_string(), rhs.to_string()); + } else if aliases.contains_key(&arg) { + println!("alias {}='{}'", arg, aliases[&arg]); + } else { + eprintln!("rush: alias: {}: not found", arg); + success = false; + } + } + success + } +} + pub fn exit(args: Vec) -> bool { match args.get(0).map_or(Ok(0), |x| x.parse::()) { Ok(n) => { diff --git a/src/helpers.rs b/src/helpers.rs index dda25dc..dba220a 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -38,6 +38,7 @@ pub struct Shell { positional: Vec, name: String, pub vars: HashMap, + pub aliases: HashMap, } impl Shell { @@ -62,6 +63,7 @@ impl Shell { positional: Vec::new(), name, vars: HashMap::new(), + aliases: HashMap::new(), } } diff --git a/src/runner.rs b/src/runner.rs index 2370230..9b98cbd 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,5 +1,7 @@ use crate::builtins; use crate::helpers::{Fd, Shell}; +use crate::lexer::Lexer; +use crate::parser::Parser; use crate::parser::{Cmd, Simple}; use os_pipe::{pipe, PipeReader, PipeWriter}; use std::process::Command; @@ -65,8 +67,27 @@ impl Runner { // but it works for now. Once I figure out what's non-ideal // about it, I'll fix it fn visit(&self, node: Cmd, stdio: CmdMeta) -> bool { + self.expand_alias_then_visit(node, stdio, None) + } + + fn expand_alias_then_visit( + &self, + node: Cmd, + stdio: CmdMeta, + prev_alias: Option, + ) -> bool { match node { - Cmd::Simple(simple) => self.visit_simple(simple, stdio), + Cmd::Simple(simple) => { + if (prev_alias.is_none() || prev_alias.as_ref().unwrap() != &simple.cmd) + && self.shell.borrow().aliases.contains_key(&simple.cmd) + { + let aliased_cmd = simple.cmd.clone(); + let expanded = self.expand_alias(simple); + self.expand_alias_then_visit(expanded, stdio, Some(aliased_cmd)) + } else { + self.visit_simple(simple, stdio) + } + } Cmd::Pipeline(cmd0, cmd1) => self.visit_pipe(*cmd0, *cmd1, stdio), Cmd::And(cmd0, cmd1) => self.visit_and(*cmd0, *cmd1, stdio), Cmd::Or(cmd0, cmd1) => self.visit_or(*cmd0, *cmd1, stdio), @@ -75,6 +96,52 @@ impl Runner { } } + fn expand_alias(&self, cmd: Simple) -> Cmd { + let aliasee = &self.shell.borrow().aliases[&cmd.cmd]; + let lexer = Lexer::new(aliasee, Rc::clone(&self.shell)); + let mut parser = Parser::new(lexer, Rc::clone(&self.shell)); + let expanded = parser.get().unwrap(); + + fn move_args(expanding: Cmd, parent: Simple) -> Cmd { + match expanding { + Cmd::Simple(mut new_simple) => { + new_simple.args.extend(parent.args); + Cmd::Simple(new_simple) + } + Cmd::Pipeline(lhs, rhs) => Cmd::Pipeline(lhs, Box::new(move_args(*rhs, parent))), + Cmd::And(lhs, rhs) => Cmd::And(lhs, Box::new(move_args(*rhs, parent))), + Cmd::Or(lhs, rhs) => Cmd::Or(lhs, Box::new(move_args(*rhs, parent))), + Cmd::Not(not) => Cmd::Not(Box::new(move_args(*not, parent))), + Cmd::Empty => Cmd::Empty, + } + } + + fn propagate_env(expanding: Cmd, parent: &Simple) -> Cmd { + match expanding { + Cmd::Simple(mut new_simple) => { + new_simple.env = parent.env.clone(); + Cmd::Simple(new_simple) + } + Cmd::Pipeline(lhs, rhs) => Cmd::Pipeline( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::And(lhs, rhs) => Cmd::And( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::Or(lhs, rhs) => Cmd::Or( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::Not(not) => Cmd::Not(Box::new(propagate_env(*not, parent))), + Cmd::Empty => Cmd::Empty, + } + } + + move_args(propagate_env(expanded, &cmd), cmd) + } + fn visit_not(&self, cmd: Cmd, stdio: CmdMeta) -> bool { let result = self.visit(cmd, stdio); !result @@ -109,9 +176,11 @@ impl Runner { fn visit_simple(&self, mut simple: Simple, stdio: CmdMeta) -> bool { self.reconcile_io(&mut simple, stdio); match &simple.cmd[..] { + "alias" => builtins::alias(&mut self.shell.borrow_mut().aliases, simple.args), "exit" => builtins::exit(simple.args), "cd" => builtins::cd(simple.args), "set" => builtins::set(simple.args, &self.shell), + command => { let mut cmd = Command::new(command); cmd.args(&simple.args); @@ -161,4 +230,3 @@ impl Runner { } } // How do I test this module? - diff --git a/tests/cli.rs b/tests/cli.rs index 1dc7fd7..0dcca53 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -23,6 +23,60 @@ where Ok(assert.stdout(output)) } +#[test] +fn alias_defines_and_prints_aliases() -> Result<(), Box> { + test_io( + "alias foo=echo +foo bar +alias +alias foo +alias boo='far' foo boo +", + "$> $> bar +$> alias foo='echo' +$> alias foo='echo' +$> alias foo='echo' +alias boo='far' +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn aliases_do_not_self_recurse() -> Result<(), Box> { + test_io( + "alias echo='echo foo' +echo bar +", + "$> $> foo bar +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn aliases_can_be_nested() -> Result<(), Box> { + test_io( + "alias foo='bar asdf && bar fdsa' +alias bar=echo +foo boo +", + "$> $> $> asdf +fdsa boo +$> +", + )? + .success() + .code(0); + Ok(()) +} + #[test] fn echo_prints_argument() -> Result<(), Box> { test_io( From 38d2af1fbe686ee1900f7938545438749db6252d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 22:53:03 +0200 Subject: [PATCH 06/14] Test expected order of alias output --- tests/cli.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 0dcca53..bf54867 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -31,12 +31,15 @@ foo bar alias alias foo alias boo='far' foo boo +alias ", "$> $> bar $> alias foo='echo' $> alias foo='echo' $> alias foo='echo' alias boo='far' +$> alias boo='far' +alias foo='echo' $> ", )? From 31ade764c7fbcb057240cd2f04cf683fb5d36835 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 22:53:48 +0200 Subject: [PATCH 07/14] Fix order of alias output --- src/builtins.rs | 4 ++-- src/helpers.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/builtins.rs b/src/builtins.rs index 9d60616..c29487d 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1,5 +1,5 @@ use regex::Regex; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::process::exit as exit_program; use std::env; use std::rc::Rc; @@ -11,7 +11,7 @@ use crate::helpers::Shell; pub fn alias( // Aliases can be added and then printed in the same command - aliases: &mut HashMap, + aliases: &mut BTreeMap, args: Vec, ) -> bool { if args.len() == 0 { diff --git a/src/helpers.rs b/src/helpers.rs index dba220a..5cbc492 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,6 +1,6 @@ use nix::unistd::Uid; use os_pipe::{dup_stderr, dup_stdin, dup_stdout, PipeReader, PipeWriter}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::env; use std::fs::{self, File, OpenOptions}; use std::io::{self, BufRead, BufReader, Write}; @@ -38,7 +38,7 @@ pub struct Shell { positional: Vec, name: String, pub vars: HashMap, - pub aliases: HashMap, + pub aliases: BTreeMap, } impl Shell { @@ -63,7 +63,7 @@ impl Shell { positional: Vec::new(), name, vars: HashMap::new(), - aliases: HashMap::new(), + aliases: BTreeMap::new(), } } From ddb0901c76ace64718f4ba993fb09c34ffb741ca Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 23:02:05 +0200 Subject: [PATCH 08/14] Implement unalias builtin --- src/builtins.rs | 22 ++++++++++++++++++++++ src/runner.rs | 1 + tests/cli.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/src/builtins.rs b/src/builtins.rs index c29487d..c6a5745 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -67,3 +67,25 @@ pub fn set(args: Vec, shell: &Rc>) -> bool { true } +pub fn unalias(aliases: &mut BTreeMap, args: Vec) -> bool { + if args.len() == 0 { + eprintln!("unalias: usage: unalias [-a] name [name ...]"); + false + } else { + if args[0] == "-a" { + aliases.clear(); + true + } else { + let mut success = true; + for arg in args { + if aliases.contains_key(&arg) { + aliases.remove(&arg); + } else { + eprintln!("rush: unalias: {}: not found", arg); + success = false; + } + } + success + } + } +} diff --git a/src/runner.rs b/src/runner.rs index 9b98cbd..c9991b1 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -180,6 +180,7 @@ impl Runner { "exit" => builtins::exit(simple.args), "cd" => builtins::cd(simple.args), "set" => builtins::set(simple.args, &self.shell), + "unalias" => builtins::unalias(&mut self.shell.borrow_mut().aliases, simple.args), command => { let mut cmd = Command::new(command); diff --git a/tests/cli.rs b/tests/cli.rs index bf54867..5e44296 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -109,3 +109,52 @@ fn exit_prints_nothing() -> Result<(), Box> { test_io("exit", "$> ")?.success().code(0); Ok(()) } + +#[test] +fn unalias_removes_alias() -> Result<(), Box> { + test_io( + "alias foo='echo' bar='oche' +alias +unalias foo +alias +unalias bar +foo foo +", + "$> $> alias bar='oche' +alias foo='echo' +$> $> alias bar='oche' +$> $> $> +", + )? + .stderr( + "rush: foo: No such file or directory (os error 2) +", + ) + .success() + .code(0); + Ok(()) +} + +#[test] +fn unalias_dash_a_removes_all_aliases() -> Result<(), Box> { + test_io( + "alias foo='echo' bar='oche' +alias +unalias -a +alias +alias foo='echo' bar='oche' +alias +unalias -a asdf +alias +", + "$> $> alias bar='oche' +alias foo='echo' +$> $> $> $> alias bar='oche' +alias foo='echo' +$> $> $> +", + )? + .success() + .code(0); + Ok(()) +} From 7a9d712afd43e543cf5721309438547ea05253f3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Sun, 11 Oct 2020 23:17:47 +0200 Subject: [PATCH 09/14] Simplify guard against recursive alias --- src/runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runner.rs b/src/runner.rs index c9991b1..a4880f3 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -78,7 +78,7 @@ impl Runner { ) -> bool { match node { Cmd::Simple(simple) => { - if (prev_alias.is_none() || prev_alias.as_ref().unwrap() != &simple.cmd) + if (prev_alias.as_ref() != Some(&simple.cmd)) && self.shell.borrow().aliases.contains_key(&simple.cmd) { let aliased_cmd = simple.cmd.clone(); From 3b33bbfc1f33b3f9f02a9bebdc4e9323eb85be89 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 30 Oct 2020 23:15:48 +0100 Subject: [PATCH 10/14] Test that aliases can be empty --- tests/cli.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 5e44296..7b34ce7 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -80,6 +80,24 @@ $> Ok(()) } +#[test] +fn alias_can_be_empty() -> Result<(), Box> { + test_io( + "alias foo= +alias +foo +foo echo bar +", + "$> $> alias foo='' +$> $> bar +$> +", + )? + .success() + .code(0); + Ok(()) +} + #[test] fn echo_prints_argument() -> Result<(), Box> { test_io( From 6192c308654bc0fc3eca27b54f16e8cbda92bdc3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 30 Oct 2020 23:16:42 +0100 Subject: [PATCH 11/14] Fix panic on executing empty alias --- src/runner.rs | 81 +++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/runner.rs b/src/runner.rs index a4880f3..35c7b96 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -97,49 +97,60 @@ impl Runner { } fn expand_alias(&self, cmd: Simple) -> Cmd { - let aliasee = &self.shell.borrow().aliases[&cmd.cmd]; - let lexer = Lexer::new(aliasee, Rc::clone(&self.shell)); + let substitution = &self.shell.borrow().aliases[&cmd.cmd]; + let lexer = Lexer::new(substitution, Rc::clone(&self.shell)); let mut parser = Parser::new(lexer, Rc::clone(&self.shell)); - let expanded = parser.get().unwrap(); - fn move_args(expanding: Cmd, parent: Simple) -> Cmd { - match expanding { - Cmd::Simple(mut new_simple) => { - new_simple.args.extend(parent.args); - Cmd::Simple(new_simple) + if let Ok(expanded) = parser.get() { + fn move_args(expanding: Cmd, parent: Simple) -> Cmd { + match expanding { + Cmd::Simple(mut new_simple) => { + new_simple.args.extend(parent.args); + Cmd::Simple(new_simple) + } + Cmd::Pipeline(lhs, rhs) => { + Cmd::Pipeline(lhs, Box::new(move_args(*rhs, parent))) + } + Cmd::And(lhs, rhs) => Cmd::And(lhs, Box::new(move_args(*rhs, parent))), + Cmd::Or(lhs, rhs) => Cmd::Or(lhs, Box::new(move_args(*rhs, parent))), + Cmd::Not(not) => Cmd::Not(Box::new(move_args(*not, parent))), + Cmd::Empty => Cmd::Empty, } - Cmd::Pipeline(lhs, rhs) => Cmd::Pipeline(lhs, Box::new(move_args(*rhs, parent))), - Cmd::And(lhs, rhs) => Cmd::And(lhs, Box::new(move_args(*rhs, parent))), - Cmd::Or(lhs, rhs) => Cmd::Or(lhs, Box::new(move_args(*rhs, parent))), - Cmd::Not(not) => Cmd::Not(Box::new(move_args(*not, parent))), - Cmd::Empty => Cmd::Empty, } - } - fn propagate_env(expanding: Cmd, parent: &Simple) -> Cmd { - match expanding { - Cmd::Simple(mut new_simple) => { - new_simple.env = parent.env.clone(); - Cmd::Simple(new_simple) + fn propagate_env(expanding: Cmd, parent: &Simple) -> Cmd { + match expanding { + Cmd::Simple(mut new_simple) => { + new_simple.env = parent.env.clone(); + Cmd::Simple(new_simple) + } + Cmd::Pipeline(lhs, rhs) => Cmd::Pipeline( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::And(lhs, rhs) => Cmd::And( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::Or(lhs, rhs) => Cmd::Or( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::Not(not) => Cmd::Not(Box::new(propagate_env(*not, parent))), + Cmd::Empty => Cmd::Empty, } - Cmd::Pipeline(lhs, rhs) => Cmd::Pipeline( - Box::new(propagate_env(*lhs, parent)), - Box::new(propagate_env(*rhs, parent)), - ), - Cmd::And(lhs, rhs) => Cmd::And( - Box::new(propagate_env(*lhs, parent)), - Box::new(propagate_env(*rhs, parent)), - ), - Cmd::Or(lhs, rhs) => Cmd::Or( - Box::new(propagate_env(*lhs, parent)), - Box::new(propagate_env(*rhs, parent)), - ), - Cmd::Not(not) => Cmd::Not(Box::new(propagate_env(*not, parent))), - Cmd::Empty => Cmd::Empty, } - } - move_args(propagate_env(expanded, &cmd), cmd) + move_args(propagate_env(expanded, &cmd), cmd) + } else { + let mut cmd = cmd; + cmd.cmd = if cmd.args.is_empty() { + "".to_string() + } else { + cmd.args.remove(0) + }; + Cmd::Simple(cmd) + } } fn visit_not(&self, cmd: Cmd, stdio: CmdMeta) -> bool { From bc2c14c51a4f00e6b044fca31913403c0b4605b3 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 30 Oct 2020 23:30:26 +0100 Subject: [PATCH 12/14] Use debug_println macro in 2 more places --- src/main.rs | 6 ++++-- src/parser.rs | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index d55de49..4c5615e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod debug; + use rush::lexer::Lexer; use rush::parser::Parser; use rush::runner::Runner; @@ -20,8 +23,7 @@ fn main() { let mut parser = Parser::new(lexer, Rc::clone(&shell)); match parser.get() { Ok(command) => { - #[cfg(debug_assertions)] // Only include when not built with `--release` flag - println!("\u{001b}[34m{:#?}\u{001b}[0m", command); + debug_println!("\u{001b}[34m{:#?}\u{001b}[0m", command); runner.execute(command, false); } diff --git a/src/parser.rs b/src/parser.rs index 5f78029..27b2525 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -341,8 +341,7 @@ impl Parser // Though subshells typically seem to inherit everything I'm keeping in my // `shell` variable at the moment? if let Ok(command) = parser.get() { - #[cfg(debug_assertions)] // Only include when not built with `--release` flag - println!("\u{001b}[33m{:#?}\u{001b}[0m", command); + debug_println!("\u{001b}[33m{:#?}\u{001b}[0m", command); let mut output = Runner::new(Rc::clone(&parser.shell)).execute(command, true).unwrap(); output = output.replace(char::is_whitespace, " "); From 0e04f78b2b82bea2fac0ffe6f4dad4f49cd5078f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 30 Oct 2020 23:32:11 +0100 Subject: [PATCH 13/14] Fix clippy lint --- src/builtins.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/builtins.rs b/src/builtins.rs index c6a5745..d8a9764 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -14,7 +14,7 @@ pub fn alias( aliases: &mut BTreeMap, args: Vec, ) -> bool { - if args.len() == 0 { + if args.is_empty() { for (lhs, rhs) in aliases { println!("alias {}='{}'", lhs, rhs); } @@ -68,24 +68,22 @@ pub fn set(args: Vec, shell: &Rc>) -> bool { } pub fn unalias(aliases: &mut BTreeMap, args: Vec) -> bool { - if args.len() == 0 { + if args.is_empty() { eprintln!("unalias: usage: unalias [-a] name [name ...]"); false + } else if args[0] == "-a" { + aliases.clear(); + true } else { - if args[0] == "-a" { - aliases.clear(); - true - } else { - let mut success = true; - for arg in args { - if aliases.contains_key(&arg) { - aliases.remove(&arg); - } else { - eprintln!("rush: unalias: {}: not found", arg); - success = false; - } + let mut success = true; + for arg in args { + if aliases.contains_key(&arg) { + aliases.remove(&arg); + } else { + eprintln!("rush: unalias: {}: not found", arg); + success = false; } - success } + success } } From 18ea02816e350007e3eaba1466e9657a8f6e9861 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 30 Oct 2020 23:41:37 +0100 Subject: [PATCH 14/14] Check alias/unalias in README todo list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 211edd0..874b43a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ I know that the name is overused, but it's too good to pass up. - [ ] Async execution `&` - [ ] Shell builtins - [ ] Normal built-ins - - [ ] `alias` `unalias` + - [x] `alias` `unalias` - [X] `cd` - [ ] etc - [ ] Special built-ins