diff --git a/Cargo.lock b/Cargo.lock index ef86815..4f74b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,24 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.4.6" @@ -65,6 +83,18 @@ name = "shell" version = "0.1.0" dependencies = [ "tempdir", + "thiserror", +] + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -77,6 +107,32 @@ dependencies = [ "remove_dir_all", ] +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 27360f4..89d1048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" [dev-dependencies] tempdir = "0.3.7" + +[dependencies] +thiserror = "1.0.65" diff --git a/src/ast.rs b/src/ast.rs index 40094d6..c4476f1 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,6 +1,6 @@ use crate::grammar::Token; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Ast { Command { command: Token, args: Vec }, Pipe { left: Box, right: Box }, diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..2360ed7 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,17 @@ +use crate::grammar::Token; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum Error { + #[error("parse error near `{0}`")] + Parse(Token), +} + +impl From for std::io::Error { + fn from(e: Error) -> std::io::Error { + match e { + Error::Parse(token) => { + std::io::Error::new(std::io::ErrorKind::InvalidInput, token.to_string()) + } + } + } +} diff --git a/src/grammar.rs b/src/grammar.rs index 26ea479..d4d3633 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -17,6 +17,12 @@ pub enum Token { // - Variable } +impl std::fmt::Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self.as_ref()) + } +} + impl AsRef for Token { fn as_ref(&self) -> &OsStr { match self { diff --git a/src/lex.rs b/src/lex.rs index a9414fc..6e73950 100644 --- a/src/lex.rs +++ b/src/lex.rs @@ -1,11 +1,9 @@ -use std::io; - -use crate::{grammar::Token, input}; +use crate::{error::Error, grammar::Token, input}; pub struct Lexer; impl Lexer { - pub fn lex(line: &str) -> io::Result> { + pub fn lex(line: &str) -> Result, Error> { let mut tokens = vec![]; let mut token = String::new(); let mut escape = false; @@ -46,10 +44,7 @@ impl Lexer { if iter.peek() == Some(&'|') { iter.next(); if iter.peek() == Some(&'|') { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "parse error near `|`", - )); + Err(Error::Parse(Token::Pipe))?; } tokens.push(Token::Or); } else { @@ -62,10 +57,7 @@ impl Lexer { if iter.peek() == Some(&'>') { iter.next(); if iter.peek() == Some(&'>') { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "parse error near `>`", - )); + Err(Error::Parse(Token::RedirectAppend))?; } tokens.push(Token::RedirectAppend); } else { @@ -78,10 +70,7 @@ impl Lexer { if iter.peek() == Some(&'&') { iter.next(); if iter.peek() == Some(&'&') { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "parse error near `&`", - )); + Err(Error::Parse(Token::Background))?; } tokens.push(Token::And); } else { diff --git a/src/lib.rs b/src/lib.rs index c869d5c..7d7fca8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod ast; +pub mod error; pub mod exec; pub mod grammar; pub mod lex; diff --git a/src/parse.rs b/src/parse.rs index 93e28b9..8e3b1ac 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,9 +1,9 @@ -use crate::{ast::Ast, grammar::Token, lex::is_operator}; +use crate::{ast::Ast, error::Error, grammar::Token, lex::is_operator}; pub struct Parser; impl Parser { - pub fn parse(tokens: &[Token]) -> Option { + pub fn parse(tokens: &[Token]) -> Result { let mut i = 0; let mut nodes = vec![]; @@ -12,7 +12,7 @@ impl Parser { match token { Token::Pipe => { - let left = nodes.pop()?; + let left = nodes.pop().ok_or(Error::Parse(Token::Pipe))?; let command = parse_command(&tokens[i + 1..]); i += command.args.len() + 1; nodes.push(Ast::Pipe { @@ -21,7 +21,7 @@ impl Parser { }); } Token::RedirectOut => { - let left = nodes.pop()?; + let left = nodes.pop().ok_or(Error::Parse(Token::RedirectOut))?; let right = tokens[i + 1].clone(); i += 1; nodes.push(Ast::RedirectOut { @@ -30,7 +30,7 @@ impl Parser { }); } Token::RedirectAppend => { - let left = nodes.pop()?; + let left = nodes.pop().ok_or(Error::Parse(Token::RedirectAppend))?; let right = tokens[i + 1].clone(); i += 1; nodes.push(Ast::RedirectAppend { @@ -39,7 +39,7 @@ impl Parser { }); } Token::And => { - let left = nodes.pop()?; + let left = nodes.pop().ok_or(Error::Parse(Token::And))?; let command = parse_command(&tokens[i + 1..]); i += command.args.len() + 1; nodes.push(Ast::And { @@ -48,7 +48,7 @@ impl Parser { }); } Token::Or => { - let left = nodes.pop()?; + let left = nodes.pop().ok_or(Error::Parse(Token::Or))?; let command = parse_command(&tokens[i + 1..]); i += command.args.len() + 1; nodes.push(Ast::Or { @@ -57,7 +57,7 @@ impl Parser { }); } Token::End => { - let left = nodes.pop()?; + let left = nodes.pop().ok_or(Error::Parse(Token::End))?; let right_tokens = &tokens[i + 1..]; if right_tokens.is_empty() { nodes.push(Ast::Sequence { @@ -75,16 +75,14 @@ impl Parser { } Token::OpenParenthesis => { let (subshell, l) = Self::parse_subshell(&tokens[i + 1..]); + let subshell = subshell?; i += l + 1; - if let Some(subshell) = subshell { - nodes.push(Ast::Subshell { - inner: Box::new(subshell), - }); - } + nodes.push(Ast::Subshell { + inner: Box::new(subshell), + }); } Token::CloseParenthesis => { - i += 1; - break; + Err(Error::Parse(Token::CloseParenthesis))?; } t if !is_operator(t) => { let command = parse_command(&tokens[i..]); @@ -99,10 +97,10 @@ impl Parser { i += 1; } - nodes.pop() + nodes.pop().ok_or(Error::Parse(Token::End)) } - fn parse_subshell(tokens: &[Token]) -> (Option, usize) { + fn parse_subshell(tokens: &[Token]) -> (Result, usize) { let mut inner_tokens = vec![]; let mut paren_level = 1; @@ -178,27 +176,19 @@ mod tests { input!("main"), ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Pipe { left, right } => { - match *left { - Ast::Command { command, args } => { - assert_eq!(command, input!("ls")); - assert_eq!(args, vec![input!("-l")]); - } - _ => panic!("Expected Command"), - } - - match *right { - Ast::Command { command, args } => { - assert_eq!(command, input!("grep")); - assert_eq!(args, vec![input!("main")]); - } - _ => panic!("Expected Command"), - } + assert_eq!( + ast, + Ast::Pipe { + left: Box::new(Ast::Command { + command: input!("ls"), + args: vec![input!("-l")], + }), + right: Box::new(Ast::Command { + command: input!("grep"), + args: vec![input!("main")], + }), } - _ => panic!("Expected Pipe"), - } + ); } #[test] @@ -211,17 +201,15 @@ mod tests { Token::CloseParenthesis, ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Subshell { inner } => match *inner { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - }, - _ => panic!("Expected Subshell"), - } + assert_eq!( + ast, + Ast::Subshell { + inner: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + } + ); // (echo foo;) let tokens = vec![ @@ -232,27 +220,18 @@ mod tests { Token::CloseParenthesis, ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Subshell { inner } => match *inner { - Ast::Sequence { left, right } => { - match *left { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - }; - - match *right { - Ast::Empty => {} - _ => panic!("Expected Empty"), - } - } - _ => panic!("Expected Sequence"), - }, - _ => panic!("Expected Subshell"), - } + assert_eq!( + ast, + Ast::Subshell { + inner: Box::new(Ast::Sequence { + left: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + right: Box::new(Ast::Empty) + }), + } + ); // (((echo foo))) let tokens = vec![ @@ -266,23 +245,19 @@ mod tests { Token::CloseParenthesis, ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Subshell { inner } => match *inner { - Ast::Subshell { inner } => match *inner { - Ast::Subshell { inner } => match *inner { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - }, - _ => panic!("Expected Subshell"), - }, - _ => panic!("Expected Subshell"), - }, - _ => panic!("Expected Subshell"), - } + assert_eq!( + ast, + Ast::Subshell { + inner: Box::new(Ast::Subshell { + inner: Box::new(Ast::Subshell { + inner: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + }), + }), + } + ); // (echo foo | cat) let tokens = vec![ @@ -294,29 +269,21 @@ mod tests { Token::CloseParenthesis, ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Subshell { inner } => match *inner { - Ast::Pipe { left, right } => { - match *left { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - }; - match *right { - Ast::Command { command, args } => { - assert_eq!(command, input!("cat")); - assert_eq!(args, vec![]); - } - _ => panic!("Expected Command"), - }; - } - _ => panic!("Expected Pipe"), - }, - _ => panic!("Expected Subshell"), - } + assert_eq!( + ast, + Ast::Subshell { + inner: Box::new(Ast::Pipe { + left: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + right: Box::new(Ast::Command { + command: input!("cat"), + args: vec![], + }), + }), + } + ); // (echo foo | cat) | cat let tokens = vec![ @@ -330,41 +297,27 @@ mod tests { input!("cat"), ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Pipe { left, right } => { - match *left { - Ast::Subshell { inner } => match *inner { - Ast::Pipe { left, right } => { - match *left { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - }; - match *right { - Ast::Command { command, args } => { - assert_eq!(command, input!("cat")); - assert_eq!(args, vec![]); - } - _ => panic!("Expected Command"), - }; - } - _ => panic!("Expected Pipe"), - }, - _ => panic!("Expected Subshell"), - }; - match *right { - Ast::Command { command, args } => { - assert_eq!(command, input!("cat")); - assert_eq!(args, vec![]); - } - _ => panic!("Expected Command"), - }; + assert_eq!( + ast, + Ast::Pipe { + left: Box::new(Ast::Subshell { + inner: Box::new(Ast::Pipe { + left: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + right: Box::new(Ast::Command { + command: input!("cat"), + args: vec![], + }), + }), + }), + right: Box::new(Ast::Command { + command: input!("cat"), + args: vec![], + }), } - _ => panic!("Expected Pipe"), - } + ); // ((echo foo | cat) | cat) let tokens = vec![ @@ -380,45 +333,29 @@ mod tests { Token::CloseParenthesis, ]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Subshell { inner } => match *inner { - Ast::Pipe { left, right } => { - match *left { - Ast::Subshell { inner } => match *inner { - Ast::Pipe { left, right } => { - match *left { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - }; - match *right { - Ast::Command { command, args } => { - assert_eq!(command, input!("cat")); - assert_eq!(args, vec![]); - } - _ => panic!("Expected Command"), - }; - } - _ => panic!("Expected Pipe"), - }, - _ => panic!("Expected Subshell"), - }; - - match *right { - Ast::Command { command, args } => { - assert_eq!(command, input!("cat")); - assert_eq!(args, vec![]); - } - _ => panic!("Expected Command"), - }; - } - _ => panic!("Expected Pipe"), - }, - _ => panic!("Expected Subshell"), - } + assert_eq!( + ast, + Ast::Subshell { + inner: Box::new(Ast::Pipe { + left: Box::new(Ast::Subshell { + inner: Box::new(Ast::Pipe { + left: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + right: Box::new(Ast::Command { + command: input!("cat"), + args: vec![], + }), + }), + }), + right: Box::new(Ast::Command { + command: input!("cat"), + args: vec![], + }), + }), + } + ); } #[test] @@ -431,7 +368,8 @@ mod tests { Token::CloseParenthesis, Token::CloseParenthesis, ]; - assert!(Parser::parse(&tokens).is_none()); + let ast = Parser::parse(&tokens); + assert_eq!(ast, Err(Error::Parse(Token::CloseParenthesis))); } #[test] @@ -439,23 +377,15 @@ mod tests { // echo foo; let tokens = vec![input!("echo"), input!("foo"), Token::End]; let ast = Parser::parse(&tokens).unwrap(); - - match ast { - Ast::Sequence { left, right } => { - match *left { - Ast::Command { command, args } => { - assert_eq!(command, input!("echo")); - assert_eq!(args, vec![input!("foo")]); - } - _ => panic!("Expected Command"), - } - - match *right { - Ast::Empty => {} - _ => panic!("Expected Command"), - } + assert_eq!( + ast, + Ast::Sequence { + left: Box::new(Ast::Command { + command: input!("echo"), + args: vec![input!("foo")], + }), + right: Box::new(Ast::Empty), } - _ => panic!("Expected Sequence"), - } + ); } } diff --git a/src/pipeline.rs b/src/pipeline.rs index 3ff8cf6..6b7e852 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -7,8 +7,7 @@ pub struct Pipeline; impl Pipeline { pub fn run(input: &str) -> io::Result { let tokens = Lexer::lex(input)?; - let ast = Parser::parse(&tokens) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "parse error"))?; + let ast = Parser::parse(&tokens)?; execute(&ast) } } @@ -70,21 +69,21 @@ mod tests { assert_eq!(&result, "foo\n"); } - // #[test] - // fn test_nested_subshells() { - // let dir = TempDir::new("").unwrap(); - // let path = dir.path().join("output.txt"); - // let input = format!( - // "((( echo foo | cat ) | cat ) | cat ) | cat > {}", - // path.to_str().unwrap() - // ); - // let status = Pipeline::run(&input).unwrap(); - // assert!(status.success()); - // let mut result = String::new(); - // File::open(&path) - // .unwrap() - // .read_to_string(&mut result) - // .unwrap(); - // assert_eq!(&result, "foo\n"); - // } + #[test] + fn test_nested_subshells() { + let dir = TempDir::new("").unwrap(); + let path = dir.path().join("output.txt"); + let input = format!( + "((( echo foo | cat ) | cat ) | cat ) | cat > {}", + path.to_str().unwrap() + ); + let status = Pipeline::run(&input).unwrap(); + assert!(status.success()); + let mut result = String::new(); + File::open(&path) + .unwrap() + .read_to_string(&mut result) + .unwrap(); + assert_eq!(&result, "foo\n"); + } }