From 4e1a835ddc444c83fa7430d251ec9d31f71e4622 Mon Sep 17 00:00:00 2001 From: Charlotte Meyer Date: Mon, 24 Oct 2022 19:27:04 +0000 Subject: [PATCH] feat: add command substitutions with () This requires PreparedCommand's to work with OsStrings rather than Strings. Acked-by: cpli Acked-by: ElKowar --- crates/oyster_builtin_proc/tests/it/main.rs | 4 +- crates/oyster_parser/src/ast.rs | 9 +- crates/oyster_parser/src/lexer.rs | 14 ++- crates/oyster_parser/src/lib.rs | 2 +- crates/oyster_parser/src/parser.rs | 49 +++++++++-- crates/oyster_parser/tests/it/ast.rs | 19 ++++ crates/oyster_parser/tests/it/cst.rs | 19 ++++ crates/oyster_parser/tests/it/lexer.rs | 36 ++++++++ crates/oyster_parser/tests/it/parser.rs | 19 ++++ .../it__ast__command_substitution.snap | 56 ++++++++++++ .../it__ast__quoted_command_substitution.snap | 59 +++++++++++++ .../it__cst__command_substitution.snap | 69 +++++++++++++++ .../it__cst__quoted_command_substitution.snap | 86 +++++++++++++++++++ ...xer__closing_parenthesis_command_mode.snap | 8 ++ ...exer__closing_parenthesis_string_mode.snap | 8 ++ ...xer__opening_parenthesis_command_mode.snap | 8 ++ ...exer__opening_parenthesis_string_mode.snap | 8 ++ .../it__parser__command_substitution.snap | 58 +++++++++++++ ...__parser__quoted_command_substitution.snap | 74 ++++++++++++++++ crates/oyster_runtime/src/builtins/mod.rs | 7 +- crates/oyster_runtime/src/lib.rs | 67 +++++++++++++-- crates/oyster_runtime/src/pipeline.rs | 29 ++++--- crates/oyster_runtime/tests/it/pipeline.rs | 34 +++++++- .../it__pipeline__command_substitution.snap | 5 ++ 24 files changed, 710 insertions(+), 37 deletions(-) create mode 100644 crates/oyster_parser/tests/it/snapshots/it__ast__command_substitution.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__ast__quoted_command_substitution.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__cst__command_substitution.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__cst__quoted_command_substitution.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_command_mode.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_string_mode.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_command_mode.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_string_mode.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__parser__command_substitution.snap create mode 100644 crates/oyster_parser/tests/it/snapshots/it__parser__quoted_command_substitution.snap create mode 100644 crates/oyster_runtime/tests/it/snapshots/it__pipeline__command_substitution.snap diff --git a/crates/oyster_builtin_proc/tests/it/main.rs b/crates/oyster_builtin_proc/tests/it/main.rs index f5f74aa..3ed2a44 100644 --- a/crates/oyster_builtin_proc/tests/it/main.rs +++ b/crates/oyster_builtin_proc/tests/it/main.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, ffi::OsStr}; use oyster_builtin_proc::builtin; use oyster_runtime::Shell; @@ -6,5 +6,5 @@ use oyster_runtime::Shell; #[test] fn normal_usage() { #[builtin(description = "some text")] - fn test(_: &mut Shell, _: &[Cow]) {} + fn test(_: &mut Shell, _: &[Cow]) {} } diff --git a/crates/oyster_parser/src/ast.rs b/crates/oyster_parser/src/ast.rs index a713781..9dcc0ef 100644 --- a/crates/oyster_parser/src/ast.rs +++ b/crates/oyster_parser/src/ast.rs @@ -27,6 +27,7 @@ pub struct Word<'a>(pub Vec>); #[derive(Debug)] pub enum WordPart<'a> { Text(&'a str), + CommandSubstitution(Code<'a>), } impl<'a> TryFrom<&'a str> for Code<'a> { @@ -129,6 +130,9 @@ fn build_tree_word<'a>( ParseEvent::StartNode(NodeKind::DQuotedString) => { build_tree_string(parser, source, pos, &mut children)? } + ParseEvent::StartNode(NodeKind::CommandSubstitution) => children.push( + WordPart::CommandSubstitution(build_tree_program(parser, source, pos)?), + ), ParseEvent::EndNode => break, ParseEvent::NewLeaf(_, len) => *pos += len, ParseEvent::Error(err, _) => return Err(err), @@ -145,7 +149,7 @@ fn build_tree_string<'a>( pos: &mut usize, children: &mut Vec>, ) -> Result<(), ParseError> { - for ev in parser { + while let Some(ev) = parser.next() { match ev { ParseEvent::NewLeaf(NodeKind::PlainText, len) => { children.push(WordPart::Text(&source[*pos..*pos + len])); @@ -155,6 +159,9 @@ fn build_tree_string<'a>( children.push(WordPart::Text(&source[*pos + 1..*pos + len])); *pos += len } + ParseEvent::StartNode(NodeKind::CommandSubstitution) => children.push( + WordPart::CommandSubstitution(build_tree_program(parser, source, pos)?), + ), ParseEvent::EndNode => break, ParseEvent::NewLeaf(_, len) => *pos += len, ParseEvent::Error(err, _) => return Err(err), diff --git a/crates/oyster_parser/src/lexer.rs b/crates/oyster_parser/src/lexer.rs index 9c42ddc..f73993e 100644 --- a/crates/oyster_parser/src/lexer.rs +++ b/crates/oyster_parser/src/lexer.rs @@ -14,6 +14,10 @@ pub enum TokenKind { Pipe, /// Double quotes. DoubleQuote, + /// Opening parenthesis. + OpeningParenthesis, + /// Closing parenthesis. + ClosingParenthesis, /// Plain text. Either outside or inside of quotes. PlainText, /// A backslash followed by another character. @@ -103,6 +107,8 @@ impl Lexer<'_> { ';' => TokenKind::Semicolon, '|' => TokenKind::Pipe, '"' => TokenKind::DoubleQuote, + '(' => TokenKind::OpeningParenthesis, + ')' => TokenKind::ClosingParenthesis, c if is_whitespace(c) => { self.eat_while(is_whitespace); @@ -125,7 +131,9 @@ impl Lexer<'_> { } _ => { - self.eat_while(|c| ![' ', '\t', '\n', ';', '|', '\\', '"'].contains(&c)); + self.eat_while(|c| { + ![' ', '\t', '\n', ';', '|', '\\', '"', '(', ')'].contains(&c) + }); TokenKind::PlainText } }; @@ -147,6 +155,8 @@ impl Lexer<'_> { Some(c) => { let kind = match c { '"' => TokenKind::DoubleQuote, + '(' => TokenKind::OpeningParenthesis, + ')' => TokenKind::ClosingParenthesis, '\\' => { self.next_char(); @@ -154,7 +164,7 @@ impl Lexer<'_> { } _ => { - self.eat_while(|c| !['"', '\\'].contains(&c)); + self.eat_while(|c| !['"', '\\', '(', ')'].contains(&c)); TokenKind::PlainText } }; diff --git a/crates/oyster_parser/src/lib.rs b/crates/oyster_parser/src/lib.rs index 12311ec..b0b373f 100644 --- a/crates/oyster_parser/src/lib.rs +++ b/crates/oyster_parser/src/lib.rs @@ -8,7 +8,7 @@ //! _terminator ::= SEMICOLON | NEWLINES | EOF //! pipeline ::= command (PIPE NEWLINES? command)* //! command ::= word+ -//! word ::= (PLAIN_TEXT|DQUOTES PLAIN_TEXT DQUOTES)+ +//! word ::= (PLAIN_TEXT|DQUOTES PLAIN_TEXT DQUOTES|OPEN_PREN program CLOSE_PREN)+ //! //! extras ::= COMMENT | WHITESPACE | BACKSLASH_N //! ``` diff --git a/crates/oyster_parser/src/parser.rs b/crates/oyster_parser/src/parser.rs index d35ca4d..fc371dc 100644 --- a/crates/oyster_parser/src/parser.rs +++ b/crates/oyster_parser/src/parser.rs @@ -11,6 +11,8 @@ pub enum ParseError { UnexpectedEof, #[error("unexpected semicolon in the middle of statement")] UnexpectedSemicolon, + #[error("unmatched closing parenthesis")] + UnexpectedClosingParenthesis, } /// Type of the node. @@ -21,11 +23,14 @@ pub enum NodeKind { Semicolon, Pipe, DoubleQuote, + OpeningParenthesis, + ClosingParenthesis, PlainText, EscapedChar, Comment, Program, + CommandSubstitution, Pipeline, Command, Word, @@ -157,48 +162,76 @@ impl Iterator for Parser<'_> { match self.stack.last() { None => None, Some(nt) => match nt { + // XXX: unify Program and CommandSubstitution to avoid duplication NodeKind::Program => match self.lookahead.kind { Whitespace => leaf!(Whitespace), Newlines => leaf!(Newlines), Semicolon => leaf!(Semicolon), Comment => leaf!(Comment), - PlainText | DoubleQuote | EscapedChar => call!(Pipeline), + PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Pipeline), Pipe => error!(UnexpectedPipe), + ClosingParenthesis => error!(UnexpectedClosingParenthesis), Eof => chain!(None, ret!()), // return silently }, + NodeKind::CommandSubstitution => match self.lookahead.kind { + Whitespace => leaf!(Whitespace), + Newlines => leaf!(Newlines), + Semicolon => leaf!(Semicolon), + Comment => leaf!(Comment), + PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Pipeline), + ClosingParenthesis => match self.stack.get(self.stack.len() - 2) { + Some(&NodeKind::DQuotedString) => { + chain_buf!(leaf!(ClosingParenthesis, String), ret!()) + } + _ => chain_buf!(leaf!(ClosingParenthesis, Command), ret!()), + }, + Pipe => error!(UnexpectedPipe), + Eof => chain_buf!(error!(UnexpectedEof), ret!()), + }, NodeKind::Pipeline => match self.lookahead.kind { Whitespace => leaf!(Whitespace), Comment => leaf!(Comment), Pipe => chain!(leaf!(Pipe), call!(PipelineCont)), - PlainText | DoubleQuote | EscapedChar => call!(Command), - Newlines | Semicolon | Eof => ret!(), + PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Command), + Newlines | Semicolon | ClosingParenthesis | Eof => ret!(), }, NodeKind::PipelineCont => match self.lookahead.kind { Whitespace => leaf!(Whitespace), Newlines => leaf!(Newlines), Comment => leaf!(Comment), - PlainText | DoubleQuote | EscapedChar => tailcall!(Command), + PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => { + tailcall!(Command) + } Semicolon => chain_buf!(chain!(error!(UnexpectedSemicolon), ret!()), ret!()), Pipe => chain!(error!(UnexpectedPipe), ret!()), + ClosingParenthesis => chain!(error!(UnexpectedClosingParenthesis), ret!()), Eof => chain!(error!(UnexpectedEof), ret!()), }, NodeKind::Command => match self.lookahead.kind { Whitespace => leaf!(Whitespace), Comment => leaf!(Comment), - PlainText | DoubleQuote | EscapedChar => call!(Word), - Newlines | Semicolon | Eof => ret!(), - Pipe => ret!(), + PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Word), + Newlines | Semicolon | Pipe | ClosingParenthesis | Eof => ret!(), }, NodeKind::Word => match self.lookahead.kind { PlainText => leaf!(PlainText), EscapedChar => leaf!(EscapedChar), DoubleQuote => chain_buf!(call!(DQuotedString), leaf!(DoubleQuote, String)), - Comment | Whitespace | Newlines | Semicolon | Pipe | Eof => ret!(), + OpeningParenthesis => { + chain_buf!(call!(CommandSubstitution), leaf!(OpeningParenthesis)) + } + Comment | Whitespace | Newlines | Semicolon | Pipe | ClosingParenthesis + | Eof => ret!(), }, NodeKind::DQuotedString => match self.lookahead.kind { PlainText => leaf!(PlainText, String), EscapedChar => leaf!(EscapedChar, String), DoubleQuote => chain_buf!(leaf!(DoubleQuote, Command), ret!()), + OpeningParenthesis => chain_buf!( + call!(CommandSubstitution), + leaf!(OpeningParenthesis, Command) + ), + ClosingParenthesis => error!(UnexpectedClosingParenthesis, String), Eof => chain_buf!(error!(UnexpectedEof, Command), ret!()), _ => unreachable!(), }, diff --git a/crates/oyster_parser/tests/it/ast.rs b/crates/oyster_parser/tests/it/ast.rs index 436b73f..9dcf1f2 100644 --- a/crates/oyster_parser/tests/it/ast.rs +++ b/crates/oyster_parser/tests/it/ast.rs @@ -170,3 +170,22 @@ fn unterminated_double_quotes() { assert_snapshot!(actual); } + +#[test] +fn command_substitution() { + let source = r#"echo (whoami)"#; + + let actual = parse(source); + + assert_snapshot!(actual); +} + +#[test] +fn quoted_command_substitution() { + let source = r#"echo "(whoami) + ""#; + + let actual = parse(source); + + assert_snapshot!(actual); +} diff --git a/crates/oyster_parser/tests/it/cst.rs b/crates/oyster_parser/tests/it/cst.rs index b1af315..a4bdb40 100644 --- a/crates/oyster_parser/tests/it/cst.rs +++ b/crates/oyster_parser/tests/it/cst.rs @@ -170,3 +170,22 @@ fn unterminated_double_quotes() { assert_snapshot!(actual); } + +#[test] +fn command_substitution() { + let source = r#"echo (whoami)"#; + + let actual = parse(source); + + assert_snapshot!(actual); +} + +#[test] +fn quoted_command_substitution() { + let source = r#"echo "(whoami) + ""#; + + let actual = parse(source); + + assert_snapshot!(actual); +} diff --git a/crates/oyster_parser/tests/it/lexer.rs b/crates/oyster_parser/tests/it/lexer.rs index 0834534..6c43425 100644 --- a/crates/oyster_parser/tests/it/lexer.rs +++ b/crates/oyster_parser/tests/it/lexer.rs @@ -108,3 +108,39 @@ fn escaped_quotes_in_string() { assert_snapshot!(actual); } + +#[test] +fn opening_parenthesis_command_mode() { + let source = r"("; + + let actual = Lexer::new(source).next_command_token(); + + assert_snapshot!(actual); +} + +#[test] +fn closing_parenthesis_command_mode() { + let source = r")"; + + let actual = Lexer::new(source).next_command_token(); + + assert_snapshot!(actual); +} + +#[test] +fn opening_parenthesis_string_mode() { + let source = r"("; + + let actual = Lexer::new(source).next_string_token(); + + assert_snapshot!(actual); +} + +#[test] +fn closing_parenthesis_string_mode() { + let source = r")"; + + let actual = Lexer::new(source).next_string_token(); + + assert_snapshot!(actual); +} diff --git a/crates/oyster_parser/tests/it/parser.rs b/crates/oyster_parser/tests/it/parser.rs index b456b24..159e05d 100644 --- a/crates/oyster_parser/tests/it/parser.rs +++ b/crates/oyster_parser/tests/it/parser.rs @@ -170,3 +170,22 @@ fn unterminated_double_quotes() { assert_snapshot!(actual); } + +#[test] +fn command_substitution() { + let source = r#"echo (whoami)"#; + + let actual = parse(source); + + assert_snapshot!(actual); +} + +#[test] +fn quoted_command_substitution() { + let source = r#"echo "(whoami) + ""#; + + let actual = parse(source); + + assert_snapshot!(actual); +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__ast__command_substitution.snap b/crates/oyster_parser/tests/it/snapshots/it__ast__command_substitution.snap new file mode 100644 index 0000000..2f6b4b8 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__ast__command_substitution.snap @@ -0,0 +1,56 @@ +--- +source: crates/oyster_parser/tests/it/ast.rs +expression: actual +--- +Ok( + Code( + [ + Pipeline( + Pipeline( + [ + Command( + [ + Word( + [ + Text( + "echo", + ), + ], + ), + Word( + [ + CommandSubstitution( + Code( + [ + Pipeline( + Pipeline( + [ + Command( + [ + Word( + [ + Text( + "whoami", + ), + ], + ), + ], + None, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ], + None, + ), + ], + ), + ), + ], + ), +) diff --git a/crates/oyster_parser/tests/it/snapshots/it__ast__quoted_command_substitution.snap b/crates/oyster_parser/tests/it/snapshots/it__ast__quoted_command_substitution.snap new file mode 100644 index 0000000..0409c0b --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__ast__quoted_command_substitution.snap @@ -0,0 +1,59 @@ +--- +source: crates/oyster_parser/tests/it/ast.rs +expression: actual +--- +Ok( + Code( + [ + Pipeline( + Pipeline( + [ + Command( + [ + Word( + [ + Text( + "echo", + ), + ], + ), + Word( + [ + CommandSubstitution( + Code( + [ + Pipeline( + Pipeline( + [ + Command( + [ + Word( + [ + Text( + "whoami", + ), + ], + ), + ], + None, + ), + ], + ), + ), + ], + ), + ), + Text( + "\n ", + ), + ], + ), + ], + None, + ), + ], + ), + ), + ], + ), +) diff --git a/crates/oyster_parser/tests/it/snapshots/it__cst__command_substitution.snap b/crates/oyster_parser/tests/it/snapshots/it__cst__command_substitution.snap new file mode 100644 index 0000000..577256e --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__cst__command_substitution.snap @@ -0,0 +1,69 @@ +--- +source: crates/oyster_parser/tests/it/cst.rs +expression: actual +--- +Tree { + kind: Program, + children: [ + Tree { + kind: Pipeline, + children: [ + Tree { + kind: Command, + children: [ + Tree { + kind: Word, + children: [ + Leaf { + kind: PlainText, + len: 4, + }, + ], + }, + Leaf { + kind: Whitespace, + len: 1, + }, + Tree { + kind: Word, + children: [ + Tree { + kind: CommandSubstitution, + children: [ + Leaf { + kind: OpeningParenthesis, + len: 1, + }, + Tree { + kind: Pipeline, + children: [ + Tree { + kind: Command, + children: [ + Tree { + kind: Word, + children: [ + Leaf { + kind: PlainText, + len: 6, + }, + ], + }, + ], + }, + ], + }, + Leaf { + kind: ClosingParenthesis, + len: 1, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__cst__quoted_command_substitution.snap b/crates/oyster_parser/tests/it/snapshots/it__cst__quoted_command_substitution.snap new file mode 100644 index 0000000..d9d1587 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__cst__quoted_command_substitution.snap @@ -0,0 +1,86 @@ +--- +source: crates/oyster_parser/tests/it/cst.rs +expression: actual +--- +Tree { + kind: Program, + children: [ + Tree { + kind: Pipeline, + children: [ + Tree { + kind: Command, + children: [ + Tree { + kind: Word, + children: [ + Leaf { + kind: PlainText, + len: 4, + }, + ], + }, + Leaf { + kind: Whitespace, + len: 1, + }, + Tree { + kind: Word, + children: [ + Tree { + kind: DQuotedString, + children: [ + Leaf { + kind: DoubleQuote, + len: 1, + }, + Tree { + kind: CommandSubstitution, + children: [ + Leaf { + kind: OpeningParenthesis, + len: 1, + }, + Tree { + kind: Pipeline, + children: [ + Tree { + kind: Command, + children: [ + Tree { + kind: Word, + children: [ + Leaf { + kind: PlainText, + len: 6, + }, + ], + }, + ], + }, + ], + }, + Leaf { + kind: ClosingParenthesis, + len: 1, + }, + ], + }, + Leaf { + kind: PlainText, + len: 9, + }, + Leaf { + kind: DoubleQuote, + len: 1, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_command_mode.snap b/crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_command_mode.snap new file mode 100644 index 0000000..6a0bb26 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_command_mode.snap @@ -0,0 +1,8 @@ +--- +source: crates/oyster_parser/tests/it/lexer.rs +expression: actual +--- +Token { + kind: ClosingParenthesis, + len: 1, +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_string_mode.snap b/crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_string_mode.snap new file mode 100644 index 0000000..6a0bb26 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__lexer__closing_parenthesis_string_mode.snap @@ -0,0 +1,8 @@ +--- +source: crates/oyster_parser/tests/it/lexer.rs +expression: actual +--- +Token { + kind: ClosingParenthesis, + len: 1, +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_command_mode.snap b/crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_command_mode.snap new file mode 100644 index 0000000..6444cb7 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_command_mode.snap @@ -0,0 +1,8 @@ +--- +source: crates/oyster_parser/tests/it/lexer.rs +expression: actual +--- +Token { + kind: OpeningParenthesis, + len: 1, +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_string_mode.snap b/crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_string_mode.snap new file mode 100644 index 0000000..6444cb7 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__lexer__opening_parenthesis_string_mode.snap @@ -0,0 +1,8 @@ +--- +source: crates/oyster_parser/tests/it/lexer.rs +expression: actual +--- +Token { + kind: OpeningParenthesis, + len: 1, +} diff --git a/crates/oyster_parser/tests/it/snapshots/it__parser__command_substitution.snap b/crates/oyster_parser/tests/it/snapshots/it__parser__command_substitution.snap new file mode 100644 index 0000000..16d9289 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__parser__command_substitution.snap @@ -0,0 +1,58 @@ +--- +source: crates/oyster_parser/tests/it/parser.rs +expression: actual +--- +[ + StartNode( + Pipeline, + ), + StartNode( + Command, + ), + StartNode( + Word, + ), + NewLeaf( + PlainText, + 4, + ), + EndNode, + NewLeaf( + Whitespace, + 1, + ), + StartNode( + Word, + ), + StartNode( + CommandSubstitution, + ), + NewLeaf( + OpeningParenthesis, + 1, + ), + StartNode( + Pipeline, + ), + StartNode( + Command, + ), + StartNode( + Word, + ), + NewLeaf( + PlainText, + 6, + ), + EndNode, + EndNode, + EndNode, + NewLeaf( + ClosingParenthesis, + 1, + ), + EndNode, + EndNode, + EndNode, + EndNode, +] diff --git a/crates/oyster_parser/tests/it/snapshots/it__parser__quoted_command_substitution.snap b/crates/oyster_parser/tests/it/snapshots/it__parser__quoted_command_substitution.snap new file mode 100644 index 0000000..a0e07c4 --- /dev/null +++ b/crates/oyster_parser/tests/it/snapshots/it__parser__quoted_command_substitution.snap @@ -0,0 +1,74 @@ +--- +source: crates/oyster_parser/tests/it/parser.rs +expression: actual +--- +[ + StartNode( + Pipeline, + ), + StartNode( + Command, + ), + StartNode( + Word, + ), + NewLeaf( + PlainText, + 4, + ), + EndNode, + NewLeaf( + Whitespace, + 1, + ), + StartNode( + Word, + ), + StartNode( + DQuotedString, + ), + NewLeaf( + DoubleQuote, + 1, + ), + StartNode( + CommandSubstitution, + ), + NewLeaf( + OpeningParenthesis, + 1, + ), + StartNode( + Pipeline, + ), + StartNode( + Command, + ), + StartNode( + Word, + ), + NewLeaf( + PlainText, + 6, + ), + EndNode, + EndNode, + EndNode, + NewLeaf( + ClosingParenthesis, + 1, + ), + EndNode, + NewLeaf( + PlainText, + 9, + ), + NewLeaf( + DoubleQuote, + 1, + ), + EndNode, + EndNode, + EndNode, + EndNode, +] diff --git a/crates/oyster_runtime/src/builtins/mod.rs b/crates/oyster_runtime/src/builtins/mod.rs index 6914e69..468b2dd 100644 --- a/crates/oyster_runtime/src/builtins/mod.rs +++ b/crates/oyster_runtime/src/builtins/mod.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::HashMap}; +use std::{borrow::Cow, collections::HashMap, ffi::OsStr}; use oyster_builtin_proc::builtin; @@ -8,11 +8,11 @@ use crate::Shell; pub struct Builtin { pub name: &'static str, pub description: &'static str, - pub fun: fn(shell: &mut Shell, args: &[Cow]), + pub fun: fn(shell: &mut Shell, args: &[Cow]), } #[builtin(description = "prints help for different builtins")] -fn help(shell: &mut Shell, _args: &[Cow]) { +fn help(shell: &mut Shell, _args: &[Cow]) { println!( r"oyster help: @@ -25,6 +25,7 @@ These are the loaded builtins:" } /// Used to register and retrieve builtins. +/// Builtin names gotta be valid unicode. #[derive(Default)] pub struct BuiltinMap(HashMap<&'static str, Builtin>); diff --git a/crates/oyster_runtime/src/lib.rs b/crates/oyster_runtime/src/lib.rs index eebe0cb..2365486 100644 --- a/crates/oyster_runtime/src/lib.rs +++ b/crates/oyster_runtime/src/lib.rs @@ -4,15 +4,25 @@ pub mod builtins; mod pipeline; -use std::io; +use std::{ + ffi::OsString, + fs::File, + io::{self, Read}, + os::unix::{ffi::OsStringExt, io::FromRawFd}, + process, +}; use builtins::BuiltinMap; use nix::{ errno::Errno, + fcntl::OFlag, + libc, sys::{ signal::{self, SaFlags, SigAction, SigHandler, Signal}, signalfd::SigSet, + wait::{self, WaitPidFlag}, }, + unistd::{self, ForkResult}, }; use oyster_parser::ast; use thiserror::Error; @@ -34,11 +44,14 @@ pub enum RuntimeError { #[error("failed to create pipe: {0}")] PipeCreationFailed(#[source] nix::Error), - #[error("command not found: {0}")] - CommandNotFound(String), + #[error("failed to dup fd: {0}")] + DupFailed(#[source] nix::Error), - #[error("permission denied: {0}")] - PermissionDenied(String), + #[error("command not found: {0:?}")] + CommandNotFound(OsString), + + #[error("permission denied: {0:?}")] + PermissionDenied(OsString), #[error("failed to spawn process: {0}")] SpawnFailed(#[source] io::Error), @@ -48,6 +61,9 @@ pub enum RuntimeError { #[error("waitpid error: {0}")] WaidPid(nix::Error), + + #[error("I/O error: {0}")] + IOError(#[source] io::Error), } pub struct Shell { @@ -79,4 +95,45 @@ impl Shell { Ok(last_status) } + + pub fn run_subshell<'a>(&mut self, code: &'a ast::Code) -> Result { + let (output, input) = + unistd::pipe2(OFlag::empty()).map_err(RuntimeError::PipeCreationFailed)?; + + match unsafe { unistd::fork() }.map_err(RuntimeError::ForkFailed)? { + ForkResult::Parent { child } => { + let _ = unistd::close(input); + let mut f = unsafe { File::from_raw_fd(output) }; + + let mut bytes = Vec::new(); + f.read_to_end(&mut bytes).map_err(RuntimeError::IOError)?; + + let bytes = match bytes.iter().position(|b| !b.is_ascii_whitespace()) { + Some(start) => { + let end = bytes + .iter() + .rposition(|b| !b.is_ascii_whitespace()) + .unwrap(); + + OsString::from_vec(bytes[start..=end].to_vec()) + } + None => OsString::new(), + }; + + wait::waitpid(child, Some(WaitPidFlag::empty())).map_err(RuntimeError::WaidPid)?; + + Ok(bytes) + } + ForkResult::Child => { + let _ = unistd::dup2(input, libc::STDOUT_FILENO); + let _ = unistd::close(input); + let _ = unistd::close(output); + if let Err(err) = self.run(code) { + eprintln!("oyster: {}", err); + } + + process::exit(0); + } + } + } } diff --git a/crates/oyster_runtime/src/pipeline.rs b/crates/oyster_runtime/src/pipeline.rs index 9709bb5..30bde57 100644 --- a/crates/oyster_runtime/src/pipeline.rs +++ b/crates/oyster_runtime/src/pipeline.rs @@ -1,6 +1,6 @@ use std::{ borrow::Cow, - ffi::{OsStr, OsString}, + ffi::OsStr, fs::File, io, os::unix::{ @@ -34,8 +34,8 @@ enum CommandKind { /// A command that's ready to be run. struct PreparedCommand<'a> { kind: CommandKind, - cmd: Cow<'a, str>, - args: Vec>, + cmd: Cow<'a, OsStr>, + args: Vec>, redirect: Redirect, stdin: Option, stdout: Option, @@ -44,17 +44,23 @@ struct PreparedCommand<'a> { impl<'a> PreparedCommand<'a> { /// Create a new PreparedCommand. - fn new(command: &'a ast::Command, shell: &Shell) -> Self { + fn new(command: &'a ast::Command, shell: &mut Shell) -> Self { let mut words = command.0.iter().map(|word| { let mut words = word.0.iter(); let mut s = match words.next().unwrap() { - ast::WordPart::Text(text) => Cow::from(*text), + ast::WordPart::Text(text) => Cow::from(OsStr::new(*text)), + ast::WordPart::CommandSubstitution(code) => { + Cow::from(shell.run_subshell(code).unwrap()) + } }; for part in words { match part { - ast::WordPart::Text(text) => s.to_mut().push_str(text), + ast::WordPart::Text(text) => s.to_mut().push(text), + ast::WordPart::CommandSubstitution(code) => { + s.to_mut().push(shell.run_subshell(code).unwrap()) + } } } @@ -64,7 +70,7 @@ impl<'a> PreparedCommand<'a> { let args = words.collect(); let redirect = command.1; - let kind = match shell.builtins().get(&cmd) { + let kind = match cmd.to_str().and_then(|cmd| shell.builtins().get(cmd)) { Some(builtin) => CommandKind::Builtin(*builtin), None => CommandKind::External, }; @@ -98,15 +104,10 @@ impl<'a> PreparedCommand<'a> { } }; - let args = self.args.iter().map(|w| match w { - Cow::Borrowed(s) => Cow::Borrowed(OsStr::new(s)), - Cow::Owned(s) => Cow::Owned(OsString::from(s)), - }); - let child = match self.kind { CommandKind::External => { - let mut cmd = Command::new(self.cmd.as_ref()); - cmd.args(args); + let mut cmd = Command::new(&self.cmd); + cmd.args(self.args); cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from)); cmd.stdout(self.stdout.map_or(Stdio::inherit(), Stdio::from)); cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from)); diff --git a/crates/oyster_runtime/tests/it/pipeline.rs b/crates/oyster_runtime/tests/it/pipeline.rs index 99b9b73..29dae32 100644 --- a/crates/oyster_runtime/tests/it/pipeline.rs +++ b/crates/oyster_runtime/tests/it/pipeline.rs @@ -1,6 +1,7 @@ use std::{ borrow::Cow, env, + ffi::OsStr, fs::File, io::{BufRead, BufReader, Write}, os::unix::io::FromRawFd, @@ -22,7 +23,7 @@ use oyster_runtime::Shell; ioctl_write_int_bad!(tiocsctty, libc::TIOCSCTTY); #[builtin(description = "test builtin")] -fn test_builtin(_: &mut Shell, _: &[Cow]) { +fn test_builtin(_: &mut Shell, _: &[Cow]) { // XXX: this is a workaround around libtest's use of io::set_output_capture let _ = write!(std::io::stdout(), "this is a test\n"); } @@ -227,3 +228,34 @@ fn builtin_redirection() { assert_snapshot!(actual); } + +#[test] +fn command_substitution() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("echo")]), + Word(vec![WordPart::CommandSubstitution(Code(vec![ + Statement::Pipeline(Pipeline(vec![Command( + vec![ + Word(vec![WordPart::Text("echo")]), + Word(vec![WordPart::Text("hello")]), + ], + Redirect::None, + )])), + ]))]), + ], + Redirect::None, + )]))]) + }; + + let actual = collect_output(|| { + let mut shell = Shell::new().unwrap(); + shell.builtins_mut().add(test_builtin); + shell.run(&ast).unwrap(); + }); + + assert_snapshot!(actual); +} diff --git a/crates/oyster_runtime/tests/it/snapshots/it__pipeline__command_substitution.snap b/crates/oyster_runtime/tests/it/snapshots/it__pipeline__command_substitution.snap new file mode 100644 index 0000000..17df64b --- /dev/null +++ b/crates/oyster_runtime/tests/it/snapshots/it__pipeline__command_substitution.snap @@ -0,0 +1,5 @@ +--- +source: crates/oyster_runtime/tests/it/pipeline.rs +expression: actual +--- +"hello\r\n"