diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1d7142d..6cf3941 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,3 +22,5 @@ jobs: run: cargo run -- -c "echo test" - name: Run tests run: cargo test + - name: Clippy + run: cargo clippy --workspace -- -D warnings \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4b91017..b96d9bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - [[package]] name = "ansi_term" version = "0.11.0" @@ -339,23 +330,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - [[package]] name = "rustyline" version = "9.0.0" @@ -626,12 +600,12 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zash" -version = "0.2.0" +version = "0.3.0" dependencies = [ "colored", "dirs", "glob", - "regex", + "libc", "rustyline", "rustyline-derive", "signal-hook", diff --git a/Cargo.toml b/Cargo.toml index c7fd649..392867d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2018" license = "GPL-3.0" name = "zash" repository = "https://github.com/robiot/zash" -version = "0.2.0" +version = "0.3.0" [dependencies] colored = "2.0.0" @@ -15,6 +15,6 @@ rustyline-derive = "0.5.0" signal-hook = "0.3.10" structopt = "0.3.25" # toml = "0.5.8" -whoami = "1.1.5" glob = "0.3.0" -regex = "1.5.4" \ No newline at end of file +whoami = "1.1.5" +libc = "0.2" \ No newline at end of file diff --git a/src/builtins/cd.rs b/src/builtins/cd.rs index 854f844..42b828a 100644 --- a/src/builtins/cd.rs +++ b/src/builtins/cd.rs @@ -1,17 +1,21 @@ use colored::Colorize; -use crate::parser; use crate::utils; -pub fn cd(args: parser::Parser) -> i32 +fn error_cd(error: T) { + utils::zash_error(format!("{}: {}", "cd".red(), error.to_string())); +} + +pub fn cd(args: Vec) -> i32 { - let homedir = utils::get_home_dir(); - let mut peekable = args.peekable(); - if let Some(new_dir) = peekable.peek().as_ref() + if args.len() > 1 { + error_cd("too many arguments"); + return 1; + } + + if let Some(dir) = args.get(0) { - let dir = new_dir.to_string().replace("~", &homedir.to_string()); - let root = std::path::Path::new(&dir); - if let Err(_) = std::env::set_current_dir(&root) { - println!("{}: no such file or directory: {}", "cd".red(), dir); + if let Err(err) = std::env::set_current_dir(&std::path::Path::new(&dir)) { + error_cd(utils::error_string(err.raw_os_error().unwrap())); return 1; } } diff --git a/src/builtins/exit.rs b/src/builtins/exit.rs index b5f44ac..4e5235a 100644 --- a/src/builtins/exit.rs +++ b/src/builtins/exit.rs @@ -1,16 +1,14 @@ -use crate::parser; use crate::utils; -pub fn exit(args: parser::Parser) -> i32 +pub fn exit(args: Vec) -> i32 { - let mut peekable = args.peekable(); - if let Some(exit_code) = peekable.peek().as_ref() + if let Some(exit_code) = args.get(0) { utils::exit(match exit_code.to_string().parse::(){ Ok(m) => m, Err(_) => { utils::zash_error("exit: numeric argument required"); - return 1; + 2 } }); } else { diff --git a/src/main.rs b/src/main.rs index c767971..6be82e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,9 @@ use signal_hook::{consts, iterator::Signals}; use structopt::StructOpt; -// mod builtins; +mod builtins; mod opts; -mod parser; +mod parsers; mod scripting; mod shell; mod utils; @@ -20,7 +20,8 @@ fn main() { let opts = opts::Opts::from_args(); if let Some(command) = opts.command { - shell::run_line(command); + let mut shell = shell::Shell::new(); + shell.run_line(command); utils::exit(0); }; diff --git a/src/parser/parser.rs b/src/parser/parser.rs deleted file mode 100644 index e725c00..0000000 --- a/src/parser/parser.rs +++ /dev/null @@ -1,43 +0,0 @@ -use super::*; -use crate::utils; -use glob::glob; - -// ^(.?/[^/ ]*)+/?$ -pub fn parse_cmd(token: String) -> Vec<(tokens::CmdTokens, String)> { - let mut combine_value = "".to_string(); - let mut result = Vec::new(); - for part in lexer::cmd_to_tokens(&token).iter().peekable() { - let mut str_part = part.1.clone(); - // Replace with enviroment variable - if part.0 == tokens::CmdTokens::Variable { - str_part = std::env::var(part.1.clone()).unwrap_or_else(|_| "".to_string()); - // Replace ~ with home dir - } else if part.0 == tokens::CmdTokens::Arg { - if str_part.starts_with("~") { - str_part = str_part.split_at(1).1.to_string(); - str_part = format!("{}{}", utils::get_home_dir(), str_part); - } - if utils::re_contains(&str_part, "^(.?/[^/ ]*)+/?$") { - if let Ok(globs) = glob(&str_part) { - for entry in globs { - if let Ok(entry1) = entry { - result.push((part.0, entry1.display().to_string())); - } - } - } - continue; - } - } - - - - - if part.2 == true { - combine_value += &str_part; - } else { - result.push((part.0, format!("{}{}", combine_value, str_part))); - combine_value.clear(); - } - } - result -} diff --git a/src/parsers/errors.rs b/src/parsers/errors.rs new file mode 100644 index 0000000..4bb88be --- /dev/null +++ b/src/parsers/errors.rs @@ -0,0 +1,10 @@ +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub struct SyntaxError; + +impl std::fmt::Display for SyntaxError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "SyntaxError: Unexpected end of input") + } +} \ No newline at end of file diff --git a/src/parser/lexer.rs b/src/parsers/lexer.rs similarity index 73% rename from src/parser/lexer.rs rename to src/parsers/lexer.rs index 2ac056c..7fd1e57 100644 --- a/src/parser/lexer.rs +++ b/src/parsers/lexer.rs @@ -1,3 +1,5 @@ +// Not really a lexer lexer but it tokenizes the input. +use super::errors::*; use super::tokens; // Line to commands @@ -16,8 +18,8 @@ enum LineTCmdState { // line_to_cmds("echo hello; echo goodbye") // [(Command, "echo hello"), (Separator, ";"), (Command, "echo goodbye")] pub fn line_to_cmds(line: &str) -> Vec<(tokens::LineToCmdTokens, std::string::String)> { - use LineTCmdState::*; use tokens::LineToCmdTokens::*; + use LineTCmdState::*; let mut result = Vec::new(); let mut token = String::new(); let mut sep_before: char = '\0'; @@ -47,17 +49,18 @@ pub fn line_to_cmds(line: &str) -> Vec<(tokens::LineToCmdTokens, std::string::St } (Normal, _) if c == '&' || c == '|' => { sep_before = c; + token.push(c); // If its eoi WaitingSeparator } (WaitingSeparator, _) => { if sep_before == c { + token.pop(); if !token.is_empty() { result.push((Command, token.trim().to_string())); } result.push((Separator, format!("{}{}", c.to_string(), c.to_string()))); token.clear(); } else { - token.push(sep_before); token.push(c); } Normal @@ -107,11 +110,7 @@ enum CmdTTokenState { } fn valid_name_check(c: char) -> bool { - if !c.is_alphanumeric() && c != '_' && c != '$' { - false - } else { - true - } + (c.is_alphanumeric() || c == '_' || c == '?') && c != '$' } pub fn is_valid_variable_name(name: String) -> bool { @@ -123,58 +122,29 @@ pub fn is_valid_variable_name(name: String) -> bool { true } -// pub fn split_variable_name(name: String) -> Vec { -// let mut return_val = vec![String::new(), String::new()]; -// let mut is_valid = true; - -// for char in name.chars() { -// if is_valid { -// if !valid_name_check(char) { -// is_valid = false; -// return_val[1].push(char); -// } else { -// return_val[0].push(char); -// } -// } else { -// return_val[1].push(char); -// } -// } -// return_val -// } - -// Not result if not used... fix - type CmdToTokensReturn = (tokens::CmdTokens, std::string::String, bool); -pub fn cmd_to_tokens(line: &str) -> Vec { +pub fn cmd_to_tokens(line: &str) -> Result> { + use tokens::CmdTokens; use CmdTTokenState::*; - use tokens::CmdTokens::*; let mut result: Vec = Vec::new(); let mut token = String::new(); let mut state: CmdTTokenState = Normal; - let mut has_command: bool = false; let mut is_definition: bool = false; for (_, c) in line.chars().enumerate() { - // println!("state: {:?} -- {}", state, token); state = match (state, c) { - (Normal, '\\') => Escaped, - (Normal, '\'') => SingleQuoted, - (Normal, '"') => DoubleQuoted, + (Normal, '\\') | (DollarVariable, '\\') => Escaped, + (Normal, '\'') | (DollarVariable, '\'') => SingleQuoted, + (Normal, '"') | (DollarVariable, '"') => DoubleQuoted, (Normal, c) if c == '>' || c == '<' || c == '|' => { if !token.is_empty() { - result.push((Command, token.trim().to_string(), false)); + result.push((CmdTokens::Normal, token.trim().to_string(), false)); } - result.push((Pipe, c.to_string(), false)); + result.push((CmdTokens::Pipe, c.to_string(), false)); token.clear(); Normal } (Normal, '=') => { - // if has_dollar { - // return Err(std::io::Error::new( - // std::io::ErrorKind::Other, - // "Redeclaration of variable can't be done with $", - // )); - // } // The value stored in token, should now be the variable name if is_valid_variable_name(token.clone()) { // ex TEST? is not valid because of the question mark @@ -183,32 +153,30 @@ pub fn cmd_to_tokens(line: &str) -> Vec { } Normal } - (Normal, '$') => { - if !token.is_empty() { - result.push((Arg, token.trim().to_string(), true)); - } - token.clear(); - DollarVariable - } - (Normal, ' ') => { + // Todo: Variables should be supported everywhere + // echo ${PATH}a + // echo $USER$PWD + // (Normal, "echo", false) + // (Variable, "USER", true) + // (Normal, "$PWD", false) + (Normal, ' ') | (Normal, '$') => { if !token.is_empty() { let token_type: tokens::CmdTokens; if is_definition { is_definition = false; - token_type = Definition; + token_type = CmdTokens::Definition; } else { - if has_command == false { - has_command = true; - token_type = Command; - } else { - token_type = Arg; - } + token_type = CmdTokens::Normal; } - result.push((token_type, token.trim().to_string(), false)); + result.push((token_type, token.trim().to_string(), c == '$')); } token.clear(); - Normal + if c == ' ' { + Normal + } else { + DollarVariable + } } (Normal, _) | (Escaped, _) => { token.push(c); @@ -216,12 +184,12 @@ pub fn cmd_to_tokens(line: &str) -> Vec { } (DollarVariable, ' ') => { token.push(c); - result.push((Variable, token.trim().to_string(), false)); + result.push((CmdTokens::Variable, token.trim().to_string(), false)); token.clear(); Normal } (DollarVariable, c) if !valid_name_check(c) => { - result.push((Variable, token.trim().to_string(), true)); + result.push((CmdTokens::Variable, token.trim().to_string(), true)); token.clear(); token.push(c); Normal @@ -249,29 +217,30 @@ pub fn cmd_to_tokens(line: &str) -> Vec { }; } - // yes this is duplicated code, I will have to figure out a way to - // make it in a better way - if state == Normal { - if !token.is_empty() { - let token_type: tokens::CmdTokens; - if is_definition { - token_type = Definition; - } else { - if has_command == false { - token_type = Command; + match state { + Normal => { + if !token.is_empty() { + let token_type: tokens::CmdTokens; + if is_definition { + token_type = CmdTokens::Definition; } else { - token_type = Arg; + token_type = CmdTokens::Normal; } + result.push((token_type, token.trim().to_string(), false)); } - result.push((token_type, token.trim().to_string(), false)); } - } - else if state == DollarVariable { - if !token.is_empty() { - result.push((Variable, token.trim().to_string(), false)); + DollarVariable => { + if !token.is_empty() { + result.push((CmdTokens::Variable, token.trim().to_string(), false)); + } + } + _ => { + // println!("{:?}", state); + // Todo: Add more information on error, SyntaxError near token Pipe + return Err(SyntaxError); } } - result + Ok(result) } #[cfg(test)] diff --git a/src/parser/mod.rs b/src/parsers/mod.rs similarity index 74% rename from src/parser/mod.rs rename to src/parsers/mod.rs index 113ec24..977b9e6 100644 --- a/src/parser/mod.rs +++ b/src/parsers/mod.rs @@ -1,3 +1,4 @@ +pub mod errors; pub mod lexer; pub mod parser; pub mod tokens; diff --git a/src/parsers/parser.rs b/src/parsers/parser.rs new file mode 100644 index 0000000..e018e13 --- /dev/null +++ b/src/parsers/parser.rs @@ -0,0 +1,143 @@ +use super::errors::*; +use super::*; +use crate::utils; +use glob::glob; + +// Todo: rustyline escape star character in filenames +// -> (Command: ["echo", "wow"]), (Separator: [">"]), (Command: ["echo", "goodbye"]) +pub fn parse_cmd(token: String, status: i32) -> Result)>> { + let mut combine_value = "".to_string(); + let mut result = Vec::new(); + let mut result_part: Vec = Vec::new(); + let mut before_token: Option = None; + let mut is_definition: bool = false; + // Todo: part should give boolean if escaped/quoted or not, for wildcards, variables and ~ + for part in lexer::cmd_to_tokens(&token)?.iter().peekable() { + before_token = match part.0 { + tokens::CmdTokens::Pipe => { + if before_token.is_none() || before_token == Some(tokens::CmdTokens::Pipe) { + return Err(SyntaxError); // Ex "> echo lol" + } + if !result_part.is_empty() { + result.push((tokens::ParseCmdTokens::Command, result_part.clone())); + result_part.clear(); + } + // The separator has to be put in a vec + result.push((tokens::ParseCmdTokens::Separator, vec![part.1.clone()])); + Some(part.0) + } + _ => { + let mut str_part = part.1.clone(); + if part.0 == tokens::CmdTokens::Definition { + is_definition = true; + } + + // Replace with enviroment variable + if part.0 == tokens::CmdTokens::Variable { + if part.1.clone() == "?" { + str_part = status.to_string(); + } else { + str_part = std::env::var(part.1.clone()).unwrap_or_else(|_| "".to_string()); + } + // Replace ~ with home dir + } else if part.0 == tokens::CmdTokens::Normal && str_part.starts_with('~') { + str_part = str_part.split_at(1).1.to_string(); + str_part = format!("{}{}", utils::get_home_dir(), str_part); + } + + if part.2 { + combine_value += &str_part; + } else { + let val = format!("{}{}", combine_value, str_part); + + if is_definition { + is_definition = false; + // For now all variables are exported / enviroment variables + // Todo: Add shell variables + let definition_parts: Vec<&str> = val.split('=').collect(); + // Could maybe happen? thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1' + std::env::set_var(definition_parts[0], definition_parts[1]); + } else { + // Glob paths. ex ./*.md + if let Ok(globs) = glob(&val) { + let mut has_entry = false; + for entry in globs.flatten() { + has_entry = true; + result_part.push(entry.display().to_string()); + } + if !has_entry { + result_part.push(val); + } + } + } + combine_value.clear(); + } + Some(part.0) + } + }; + } + // Ex "hello |" + if before_token == Some(tokens::CmdTokens::Pipe) { + return Err(SyntaxError); + } + + if !result_part.is_empty() { + result.push((tokens::ParseCmdTokens::Command, result_part.clone())); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + macro_rules! string_vec { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + + #[test] + fn test_parser() { + use super::parse_cmd; + use super::tokens::ParseCmdTokens::*; + + let v = vec![ + ( + "echo hello world", + vec![(Command, string_vec!["echo", "hello", "world"])], + ), // Trim input + ( + "echo $tesrakijds", + vec![(Command, string_vec!["echo", "hello"])], + ), // Test enviroment variables + ( + "echo /home/$tesrakijds", + vec![(Command, string_vec!["echo", "/home/hello"])], + ), // Test combine with one before + ( + "echo $tesrakijds/.config", + vec![(Command, string_vec!["echo", "hello/.config"])], + ), // Test combine with one after + ( + "echo /home/$tesrakijds/.config", + vec![(Command, string_vec!["echo", "/home/hello/.config"])], + ), // Test combine with one before & one after + ( + "echo 'hello world'", + vec![(Command, string_vec!["echo", "hello world"])], + ), // Single quotes + ( + "echo \"hello world\"", + vec![(Command, string_vec!["echo", "hello world"])], + ), // Double Quotes + ( + "echo hello\\ world", + vec![(Command, string_vec!["echo", "hello world"])], + ), // Escaped space + ("TEST=$tesrakijds:/root/.config", vec![]), // Define variable with another variable + ]; + + std::env::set_var("tesrakijds", "hello"); // Random name, for enviroment variables test + for (l, r) in v { + assert_eq!(parse_cmd(l.to_string(), 0).unwrap(), r); + } + } +} diff --git a/src/parser/tokens.rs b/src/parsers/tokens.rs similarity index 69% rename from src/parser/tokens.rs rename to src/parsers/tokens.rs index d3a26ed..6d08d76 100644 --- a/src/parser/tokens.rs +++ b/src/parsers/tokens.rs @@ -6,9 +6,14 @@ pub enum LineToCmdTokens { #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum CmdTokens { - Command, - Arg, + Normal, Pipe, Definition, Variable, } + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum ParseCmdTokens { + Command, + Separator, +} \ No newline at end of file diff --git a/src/scripting.rs b/src/scripting.rs index 6aea04a..2637159 100644 --- a/src/scripting.rs +++ b/src/scripting.rs @@ -14,10 +14,9 @@ where } pub fn run_file(filename: String) -> std::io::Result<()> { - for line in read_lines(filename)? { - if let Ok(ip) = line { - shell::run_line(ip); - } + let mut shell = shell::Shell::new(); + for line in (read_lines(filename)?).flatten() { + shell.run_line(line); } Ok(()) } @@ -27,20 +26,15 @@ pub fn load_rc(homedir: String) { if !Path::new(&rcpath).exists() { let welcometext = "Welcome to zash"; println!("{}", welcometext); - let mut file = match OpenOptions::new() - .create_new(true) - .write(true) - .open(rcpath) { - Ok(m) => m, - Err(err) => { - utils::zash_error(err); - return; - } - }; - writeln!(file, "echo {}", welcometext).unwrap(); - } else { - if let Err(err) = run_file(rcpath.to_string()) { - utils::zash_error(format!("{}: {}", rcpath, err)); + let mut file = match OpenOptions::new().create_new(true).write(true).open(rcpath) { + Ok(m) => m, + Err(err) => { + utils::zash_error(err); + return; + } }; - } + writeln!(file, "echo {}", welcometext).unwrap(); + } else if let Err(err) = run_file(rcpath.to_string()) { + utils::zash_error(format!("{}: {}", rcpath, err)); + }; } diff --git a/src/shell.rs b/src/shell.rs index b32f80a..acc72dd 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,4 +1,5 @@ use colored::Colorize; +use parsers::tokens::*; use rustyline::completion::{Completer, Pair, ShellCompleter}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; @@ -8,92 +9,121 @@ use rustyline::validate::{MatchingBracketValidator, Validator}; use rustyline::{CompletionType, Config, Context, EditMode, Editor}; use rustyline_derive::Helper; use std::borrow::Cow::{self, Borrowed, Owned}; -use parser::tokens::*; -// use std::process::{Child, Command, Stdio}; +use std::process::{Child, Command, Stdio}; +// use std::collections::HashMap; - -// use crate::builtins; -use crate::parser; +use crate::builtins; +use crate::parsers; use crate::scripting; use crate::utils; -pub fn run_line(line: String) { - let mut status = 0; - let mut sep = String::new(); - for token in parser::lexer::line_to_cmds(line.trim()) { - if token.0 == LineToCmdTokens::Separator { - sep = token.1.clone(); - continue; - } +#[derive(Debug, Clone)] +pub struct Shell { + // pub variables: HashMap, + pub status: i32, +} - // "&& "Don't run the other commands if the one before failed - // - // "||" = "Or" - // "ls || dir" - // If ls does not succed it runs "dir" - // If ls succed it does not run dir - if (sep == "&&" && status != 0) || (sep == "||" && status == 0) { - break; +impl Shell { + pub fn new() -> Self { + Self { + // variables: HashMap::new() + status: 0, } - - status = exec_command(token); } -} -fn exec_command(token: (LineToCmdTokens, std::string::String)) -> i32 { - let parts = parser::parser::parse_cmd(token.1.clone()); - - println!("{:#?}", parts); - - // Add up args until Pipe? - - // let mut pipe_commands = parser::Parser::new(token.1.trim(), "|".to_string(), true).peekable(); - // let mut prev_command = None; - // while let Some(pipe_command) = pipe_commands.next() { - // let mut args = parser::Parser::new(pipe_command.trim(), " ".to_string(), false); - // let command = match args.next() { - // Some(n) => n, - // None => return 1, - // }; - // let status = match command.as_ref() { - // // Builtins - // "cd" => builtins::cd::cd(args), - // "exit" => builtins::exit::exit(args), - // command => { - // let stdin = prev_command.map_or(Stdio::inherit(), |output: Child| { - // Stdio::from(output.stdout.unwrap()) - // }); - // let stdout = if pipe_commands.peek().is_some() { - // Stdio::piped() - // } else { - // Stdio::inherit() - // }; - // match Command::new(command) - // .args(args) - // .stdin(stdin) - // .stdout(stdout) - // .spawn() - // { - // Ok(output) => { - // prev_command = Some(output); - // } - // Err(_) => { - // // prev_command = None; - // utils::zash_error(format!("command not found: {}", command)); - // return 1; - // } - // }; - // 0 - // } - // }; - // if status != 0 { - // return status; - // } - // } - // if let Some(mut final_command) = prev_command { - // final_command.wait().unwrap(); - // } - 0 + pub fn run_line(&mut self, line: String) { + let mut sep = String::new(); + for token in parsers::lexer::line_to_cmds(line.trim()) { + if token.0 == LineToCmdTokens::Separator { + sep = token.1.clone(); + continue; + } + // "&& "Don't run the other commands if the one before failed + // + // "||" = "Or" + // "ls || dir" + // If ls does not succed it runs "dir" + // If ls succed it does not run dir + if (sep == "&&" && self.status != 0) || (sep == "||" && self.status == 0) { + break; + } + self.status = Self::exec_command(self, token); + } + } + fn exec_command(&mut self, token: (LineToCmdTokens, std::string::String)) -> i32 { + let parts = match parsers::parser::parse_cmd(token.1, self.status) { + Ok(m) => m, + Err(err) => { + utils::zash_error(err); + return 0; + } + }; + // println!("{:?}", parts); + let mut prev_command = None; + for (i, part) in parts.iter().enumerate() { + match part.0 { + ParseCmdTokens::Command => { + let mut args = part.1.clone(); + // Probably a very hacky way of getting first arg then removing it + let clon = args.clone(); // So it lives longer + let command = match clon.get(0) { + Some(m) => m, + None => return 1, + }; + args.remove(0); + let status = match command.as_ref() { + // Builtins + "cd" => builtins::cd::cd(args), + "exit" => builtins::exit::exit(args), + command => { + let stdin = prev_command.map_or(Stdio::inherit(), |output: Child| { + Stdio::from(output.stdout.unwrap()) + }); + let stdout = if parts.get(i + 1).is_some() + && parts.get(i + 1).unwrap().0 == ParseCmdTokens::Separator + { + Stdio::piped() + } else { + Stdio::inherit() + }; + match Command::new(command) + .args(args.clone()) + .stdin(stdin) + .stdout(stdout) + .spawn() + { + Ok(output) => { + prev_command = Some(output); + } + Err(_) => { + // prev_command = None; + utils::zash_error(format!("command not found: {}", command)); + return 1; + } + }; + 0 + } + }; + if status != 0 { + return status; + } + } + ParseCmdTokens::Separator => { + // Maybe do something with redirects + if part.1.clone() != vec!["|"] { + utils::zash_error("this feature is currently not implemented"); + return 1; + } + } + } + } + if let Some(final_command) = prev_command { + if let Some(status_code) = final_command.wait_with_output().unwrap().status.code() { + return status_code; + } + } + 127 + } } #[derive(Helper)] @@ -140,7 +170,7 @@ impl Highlighter for ShellHelper { } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - Owned(format!("{}", hint.dimmed()).to_owned()) + Owned(format!("{}", hint.dimmed())) } fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { @@ -192,6 +222,7 @@ pub fn shell() { if rl.load_history(&hispath).is_err() { utils::zash_error("No previous history"); } + let mut shell = Shell::new(); loop { let mut current_dir = std::env::current_dir().unwrap().display().to_string(); @@ -210,11 +241,10 @@ pub fn shell() { ); rl.helper_mut().expect("No helper").prompt = p.to_string(); let readline = rl.readline(&p); - match readline { Ok(line) => { rl.add_history_entry(line.as_str()); - run_line(line); + shell.run_line(line); } Err(ReadlineError::Interrupted) => { continue; diff --git a/src/utils.rs b/src/utils.rs index 576de9a..f024fb2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,32 @@ use colored::Colorize; use dirs::home_dir; +use std::ffi::CStr; +use std::os::raw::{c_char, c_int}; +use std::str; + +// https://stackoverflow.com/questions/40710115/how-does-one-get-the-error-message-as-provided-by-the-system-without-the-os-err +// from https://github.com/rust-lang/rust/blob/1.26.2/src/libstd/sys/unix/os.rs#L87-L107 +pub fn error_string(errno: i32) -> String { + extern "C" { + #[cfg_attr( + any(target_os = "linux", target_env = "newlib"), + link_name = "__xpg_strerror_r" + )] + fn strerror_r(errnum: c_int, buf: *mut c_char, buflen: libc::size_t) -> c_int; + } + + let mut buf = [0 as c_char; 128]; + + let p = buf.as_mut_ptr(); + unsafe { + assert!(!(strerror_r(errno as c_int, p, buf.len()) < 0), "strerror_r failure"); + + let p = p as *const _; + str::from_utf8(CStr::from_ptr(p).to_bytes()) + .unwrap() + .to_owned() + } +} pub fn exit(code: i32) { std::process::exit(code); @@ -9,7 +36,6 @@ pub fn zash_error(error: T) { eprintln!("{}: {}", "zash".red(), error.to_string()); } - pub fn get_home_dir() -> String { if home_dir().is_none() { zash_error( @@ -20,17 +46,3 @@ pub fn get_home_dir() -> String { let homedir_pathbuf = home_dir().unwrap(); return homedir_pathbuf.display().to_string(); } - -pub fn re_contains(text: &str, ptn: &str) -> bool { - let re; - match regex::Regex::new(ptn) { - Ok(x) => { - re = x; - } - Err(e) => { - zash_error(format!("Regex new error: {:?}", e)); - return false; - } - } - re.is_match(text) -} \ No newline at end of file