├── tests └── .gitkeep ├── .gitignore ├── src ├── utils │ ├── mod.rs │ ├── auto_complete.rs │ ├── lexer.rs │ └── parser.rs └── main.rs ├── Cargo.toml ├── README.md └── Cargo.lock /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lexer; 2 | pub mod parser; 3 | pub mod auto_complete; 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["http://radiux.io/"] 3 | name = "rush" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | nix = "0.8.0" 8 | regex = "0.2.1" 9 | -------------------------------------------------------------------------------- /src/utils/auto_complete.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::io; 3 | 4 | pub fn detect_tab() { 5 | 6 | } 7 | 8 | pub fn find_keyword_matches(s: &str) -> &str { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rush 2 | 3 | ## What is Rush? 4 | 5 | Rush is a command line shell written in Rust. 6 | 7 | ## Setting it up 8 | 9 | 1. Install `rustup` if you haven't already. 10 | 2. Clone the repo. 11 | 3. `cargo run` 12 | 13 | ## Completed 14 | - [x] Execution of `$PATH` commands 15 | - [x] Directory navigation 16 | - [x] Shell sleep 17 | - [x] Read command history 18 | 19 | ## In Progress 20 | 21 | - [ ] Recursive descent parser 22 | - [ ] I/O redirection 23 | - [ ] Store command history 24 | 25 | ## Road Map 26 | 27 | - [ ] Signaling 28 | - [ ] Run commands in background through `&` and `wait` 29 | - [ ] Execute programs 30 | - [ ] Autocomplete 31 | - [ ] Forking shell processes 32 | - [ ] Buffered Input 33 | - [ ] Pipes 34 | - [ ] Aliasing 35 | - [ ] Parallel commands 36 | -------------------------------------------------------------------------------- /src/utils/lexer.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | #[derive(Debug, Eq, PartialEq)] 4 | enum Token { 5 | PIPE, 6 | AMPERSAND, 7 | OUTFILE, 8 | INFILE, 9 | TERMINATE, 10 | ID, 11 | } 12 | 13 | struct Lexer; 14 | 15 | pub impl Lexer { 16 | fn grab_tokens(&self, input: &str) -> Vec<&str> { 17 | let tokens = Regex::new(r"-?[a-zA-Z]+|[.<>|&$]+").unwrap(); 18 | let matches: Vec<&str> = tokens 19 | .find_iter(input) 20 | .map(|x| x.as_str()) 21 | .collect(); 22 | 23 | matches 24 | } 25 | fn tokenize(&self, grammar: &str) -> Vec { 26 | let mut iterator = grammar.chars(); 27 | let mut tokens = vec![]; 28 | 29 | while let Some(item) = iterator.next() { 30 | match item { 31 | '|' => tokens.push(Token::PIPE), 32 | '&' => tokens.push(Token::AMPERSAND), 33 | '>' => tokens.push(Token::OUTFILE), 34 | '<' => tokens.push(Token::INFILE), 35 | ';' => tokens.push(Token::TERMINATE), 36 | '$' => tokens.push(Token::ID), 37 | _ => { } 38 | } 39 | } 40 | 41 | tokens 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/parser.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use ast::{self, Grammar}; 3 | 4 | use regex::Regex; 5 | 6 | struct Parser; 7 | 8 | pub impl Parser { 9 | fn parse(&self, tokens: Vec, rule_count: i32) -> Result { 10 | let mut grammar_object = Grammar { rules: vec![], main: 0 }; 11 | let mut insert_order = vec![]; 12 | let mut i = 0; 13 | 14 | while let Some(token) = tokens.get(i) { 15 | if let &Token::Name(id) = token { 16 | i += 1; 17 | if let Some(brace_token) = tokens.get(i) { 18 | i += 1; 19 | if brace_token == &Token::OpenBrace { 20 | let pattern = parse_expression(&mut i, &tokens); 21 | match tokens.get(i) { 22 | Some(&Token::CloseBrace) => i += 1, 23 | _ => return Err(i) 24 | } 25 | match pattern { 26 | Ok(p) => insert_order.push((id, p)), 27 | Err(x) => return Err(x) 28 | } 29 | } else { 30 | return Err(i); 31 | } 32 | } else { 33 | return Err(i); 34 | } 35 | } else { 36 | return Err(i); 37 | } 38 | } 39 | 40 | insert_order.sort_by(|a, b| a.0.cmp(&b.0)); 41 | 42 | if insert_order.iter().map(|x| x.0).eq(0..rule_count) { 43 | grammar_object.rules = insert_order.drain(..).map(|x| x.1).collect(); 44 | Ok(grammar_object) 45 | } else { 46 | Err(i) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "rush" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "nix 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "aho-corasick" 11 | version = "0.6.3" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | dependencies = [ 14 | "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "bitflags" 19 | version = "0.7.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | 22 | [[package]] 23 | name = "cfg-if" 24 | version = "0.1.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | 27 | [[package]] 28 | name = "kernel32-sys" 29 | version = "0.2.2" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | dependencies = [ 32 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 33 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 34 | ] 35 | 36 | [[package]] 37 | name = "libc" 38 | version = "0.2.21" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | 41 | [[package]] 42 | name = "memchr" 43 | version = "1.0.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | dependencies = [ 46 | "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 47 | ] 48 | 49 | [[package]] 50 | name = "nix" 51 | version = "0.8.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | dependencies = [ 54 | "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 55 | "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 56 | "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 57 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 58 | ] 59 | 60 | [[package]] 61 | name = "regex" 62 | version = "0.2.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | dependencies = [ 65 | "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", 66 | "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 67 | "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 70 | ] 71 | 72 | [[package]] 73 | name = "regex-syntax" 74 | version = "0.4.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | 77 | [[package]] 78 | name = "thread-id" 79 | version = "3.0.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | dependencies = [ 82 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 84 | ] 85 | 86 | [[package]] 87 | name = "thread_local" 88 | version = "0.3.3" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | dependencies = [ 91 | "thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 92 | "unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 93 | ] 94 | 95 | [[package]] 96 | name = "unreachable" 97 | version = "0.1.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | dependencies = [ 100 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 101 | ] 102 | 103 | [[package]] 104 | name = "utf8-ranges" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | 108 | [[package]] 109 | name = "void" 110 | version = "1.0.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | 113 | [[package]] 114 | name = "winapi" 115 | version = "0.2.8" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | 118 | [[package]] 119 | name = "winapi-build" 120 | version = "0.1.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | 123 | [metadata] 124 | "checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699" 125 | "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" 126 | "checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" 127 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 128 | "checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" 129 | "checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" 130 | "checksum nix 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "314a973a94409e39b78fa3a9cfb4ec4831791879079af087f64bf01394dc6c03" 131 | "checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01" 132 | "checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457" 133 | "checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a" 134 | "checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7" 135 | "checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91" 136 | "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" 137 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 138 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 139 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 140 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_must_use)] 2 | #![allow(unused_imports)] 3 | extern crate nix; 4 | extern crate regex; 5 | 6 | use std::{thread, time}; 7 | use std::fs::{self, File, OpenOptions}; 8 | use std::io::{self, BufReader, Write}; 9 | use std::io::prelude::*; 10 | use std::os::unix; 11 | use std::path::{self, Path}; 12 | use std::env; 13 | use std::process::{Command, Stdio}; 14 | use std::ffi::OsString; 15 | 16 | use nix::unistd::{fork, ForkResult}; 17 | 18 | struct Shell; 19 | 20 | impl Shell { 21 | fn change_dir(&self, input: &str) { 22 | let root = env::home_dir().unwrap(); 23 | let c_dir = env::current_dir().unwrap(); 24 | let mut prev_dirs = c_dir.clone(); 25 | prev_dirs.pop(); 26 | let prev_dir = prev_dirs.join(""); 27 | let path = Path::new(input); 28 | 29 | if input.is_empty() { 30 | env::set_current_dir(root) 31 | .expect("No home directory found") 32 | } else if !path.is_dir() { 33 | println!("-rush: cd: {}: No such directory", input); 34 | } else { 35 | match input { 36 | "~" | "~/" => env::set_current_dir(&root).unwrap(), 37 | "." => env::set_current_dir(&c_dir).unwrap(), 38 | ".." => env::set_current_dir(&prev_dir).unwrap(), 39 | _ => env::set_current_dir(path).unwrap() 40 | } 41 | } 42 | } 43 | fn save_history(&self, cmd: &str) -> io::Result<()> { 44 | let root = env::home_dir().unwrap(); 45 | let bash_history = [root.to_str().unwrap(), "/.bash_history"].join(""); 46 | let mut f = OpenOptions::new().append(true).open(bash_history)?; 47 | 48 | f.write_all([cmd, "\n"].join("").as_bytes())?; 49 | Ok(()) 50 | } 51 | fn get_history(&self) { 52 | let root = env::home_dir().unwrap(); 53 | let bash_history = [root.to_str().unwrap(), "/.bash_history"].join(""); 54 | let mut f = File::open(bash_history).unwrap(); 55 | let mut content = String::new(); 56 | 57 | f.read_to_string(&mut content); 58 | 59 | print!("{}", content); 60 | } 61 | } 62 | 63 | fn exec_cmd(bin_path: &str, argument: &str, input: &str) { 64 | let mut builder = Command::new(bin_path); 65 | 66 | if !argument.is_empty() { 67 | builder.arg(argument); 68 | } 69 | 70 | if !input.is_empty() { 71 | let child = builder 72 | .output() 73 | .unwrap_or_else(|e| { 74 | panic!("Failed to execute process: {}", e); 75 | }); 76 | 77 | if child.status.success() { 78 | let s = String::from_utf8_lossy(&child.stdout); 79 | let mut f = File::create(&Path::new(&input)).unwrap(); 80 | f.write_all(s.as_bytes()); 81 | } 82 | } else { 83 | let mut child = builder 84 | .stdout(Stdio::inherit()) 85 | .spawn() 86 | .expect("Command didn't execute successfully"); 87 | 88 | let ecode = child.wait().unwrap_or_else(|e| { 89 | panic!("Failed to wait on child: {}", e); 90 | }); 91 | 92 | assert!(ecode.success()); 93 | } 94 | } 95 | 96 | fn visit_dir(dir: &Path, cmd: &str, argument: &str, input: &str) -> io::Result<()> { 97 | if dir.is_dir() { 98 | for entry in fs::read_dir(dir)? { 99 | let empty_dir = OsString::new(); 100 | let entry = entry?; 101 | let path = entry.path(); 102 | let bin = path.iter().last().unwrap_or(&empty_dir); 103 | 104 | if bin == cmd { 105 | exec_cmd(path.to_str().unwrap(), argument, input); 106 | return Ok(()); 107 | } 108 | } 109 | } 110 | Ok(()) 111 | } 112 | 113 | fn find_path_cmd(cmd: &str, argument: &str, input: &str) { 114 | let key = "PATH"; 115 | 116 | match env::var_os(key) { 117 | Some(paths) => { 118 | let mut directories = env::split_paths(&paths).collect::>(); 119 | directories.sort(); 120 | directories.dedup(); 121 | for path in directories.iter() { 122 | visit_dir(&path, &cmd, &argument, &input); 123 | } 124 | }, 125 | None => println!("{} is not defined in the environment", key) 126 | } 127 | } 128 | 129 | fn touch(path: &Path) -> io::Result<()> { 130 | match OpenOptions::new() 131 | .create(true) 132 | .write(true) 133 | .open(path) { 134 | Ok(_) => Ok(()), 135 | Err(e) => Err(e) 136 | } 137 | } 138 | 139 | #[test] 140 | fn return_home_if_empty_arg() { 141 | let path = Path::new("/usr/"); 142 | let prev_dir = env::set_current_dir(path); 143 | Shell.change_dir(""); 144 | let root = env::home_dir().unwrap(); 145 | let c_dir = env::current_dir().unwrap(); 146 | assert_eq!(root, c_dir); 147 | } 148 | 149 | fn main() { 150 | let stdin = std::io::stdin(); 151 | let stdout = std::io::stdout(); 152 | let mut stdin = stdin.lock(); 153 | let mut stdout = stdout.lock(); 154 | let mut buffer = String::new(); 155 | let empty_dir = OsString::new(); 156 | 157 | println!("** rush **\n"); 158 | 159 | loop { 160 | let root_dir = env::home_dir().unwrap(); 161 | let curr_dir = env::current_dir().unwrap(); 162 | let last_dir = curr_dir.iter().last().unwrap_or(&empty_dir); 163 | 164 | let output = if curr_dir == root_dir { 165 | "rush:~ λ ".to_string() 166 | } else { 167 | ["rush:", last_dir.to_str().unwrap(), " λ "].join("") 168 | }; 169 | 170 | write!(stdout, "{}", output); 171 | stdout.flush(); 172 | buffer.clear(); 173 | 174 | stdin 175 | .read_line(&mut buffer) 176 | .expect("Failed to parse command"); 177 | 178 | let commands = buffer 179 | .trim() 180 | .split_whitespace() 181 | .collect::>(); 182 | 183 | let command = &commands[0] as &str; 184 | let argument = commands.get(1).cloned().unwrap_or(""); 185 | let argument2 = commands.get(2).cloned().unwrap_or(""); 186 | let input = commands.get(3).cloned().unwrap_or(""); 187 | 188 | Shell.save_history(&command); 189 | 190 | if argument == ">" { 191 | find_path_cmd(command, "", argument2); 192 | } else if argument2 == ">" { 193 | find_path_cmd(command, argument, input); 194 | } else { 195 | match command { 196 | "pwd" => println!("{}", curr_dir.display()), 197 | "cd" => Shell.change_dir(argument), 198 | "touch" => touch(&Path::new(argument)) 199 | .unwrap_or_else(|why| { 200 | println!("! {:?}", why.kind()); 201 | }), 202 | "rm" => fs::remove_file(argument) 203 | .unwrap_or_else(|why| { 204 | println!("! {:?}", why.kind()); 205 | }), 206 | "mkdir" => fs::create_dir(argument) 207 | .unwrap_or_else(|why| { 208 | println!("! {:?}", why.kind()) 209 | }), 210 | "rmdir" => fs::remove_dir(argument) 211 | .unwrap_or_else(|why| { 212 | println!("! {:?}", why.kind()); 213 | }), 214 | "history" => Shell.get_history(), 215 | "sleep" => { 216 | let input_time = argument.parse::().unwrap(); 217 | let sleep_time = time::Duration::from_millis(input_time * 1000); 218 | let now = time::Instant::now(); 219 | 220 | thread::sleep(sleep_time); 221 | println!("Sleep {:?}", now.elapsed()); 222 | }, 223 | "help" => println!("Sorry, you're on your own for now"), 224 | "exit" => break, 225 | _ => find_path_cmd(command, argument, "") 226 | } 227 | } 228 | } 229 | } 230 | --------------------------------------------------------------------------------