feat: add command substitutions with ()
This requires PreparedCommand's to work with OsStrings rather than Strings. Acked-by: cpli Acked-by: ElKowar
This commit is contained in:
parent
65b50557c9
commit
4e1a835ddc
24 changed files with 710 additions and 37 deletions
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, ffi::OsStr};
|
||||||
|
|
||||||
use oyster_builtin_proc::builtin;
|
use oyster_builtin_proc::builtin;
|
||||||
use oyster_runtime::Shell;
|
use oyster_runtime::Shell;
|
||||||
|
@ -6,5 +6,5 @@ use oyster_runtime::Shell;
|
||||||
#[test]
|
#[test]
|
||||||
fn normal_usage() {
|
fn normal_usage() {
|
||||||
#[builtin(description = "some text")]
|
#[builtin(description = "some text")]
|
||||||
fn test(_: &mut Shell, _: &[Cow<str>]) {}
|
fn test(_: &mut Shell, _: &[Cow<OsStr>]) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ pub struct Word<'a>(pub Vec<WordPart<'a>>);
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WordPart<'a> {
|
pub enum WordPart<'a> {
|
||||||
Text(&'a str),
|
Text(&'a str),
|
||||||
|
CommandSubstitution(Code<'a>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a str> for Code<'a> {
|
impl<'a> TryFrom<&'a str> for Code<'a> {
|
||||||
|
@ -129,6 +130,9 @@ fn build_tree_word<'a>(
|
||||||
ParseEvent::StartNode(NodeKind::DQuotedString) => {
|
ParseEvent::StartNode(NodeKind::DQuotedString) => {
|
||||||
build_tree_string(parser, source, pos, &mut children)?
|
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::EndNode => break,
|
||||||
ParseEvent::NewLeaf(_, len) => *pos += len,
|
ParseEvent::NewLeaf(_, len) => *pos += len,
|
||||||
ParseEvent::Error(err, _) => return Err(err),
|
ParseEvent::Error(err, _) => return Err(err),
|
||||||
|
@ -145,7 +149,7 @@ fn build_tree_string<'a>(
|
||||||
pos: &mut usize,
|
pos: &mut usize,
|
||||||
children: &mut Vec<WordPart<'a>>,
|
children: &mut Vec<WordPart<'a>>,
|
||||||
) -> Result<(), ParseError> {
|
) -> Result<(), ParseError> {
|
||||||
for ev in parser {
|
while let Some(ev) = parser.next() {
|
||||||
match ev {
|
match ev {
|
||||||
ParseEvent::NewLeaf(NodeKind::PlainText, len) => {
|
ParseEvent::NewLeaf(NodeKind::PlainText, len) => {
|
||||||
children.push(WordPart::Text(&source[*pos..*pos + 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]));
|
children.push(WordPart::Text(&source[*pos + 1..*pos + len]));
|
||||||
*pos += len
|
*pos += len
|
||||||
}
|
}
|
||||||
|
ParseEvent::StartNode(NodeKind::CommandSubstitution) => children.push(
|
||||||
|
WordPart::CommandSubstitution(build_tree_program(parser, source, pos)?),
|
||||||
|
),
|
||||||
ParseEvent::EndNode => break,
|
ParseEvent::EndNode => break,
|
||||||
ParseEvent::NewLeaf(_, len) => *pos += len,
|
ParseEvent::NewLeaf(_, len) => *pos += len,
|
||||||
ParseEvent::Error(err, _) => return Err(err),
|
ParseEvent::Error(err, _) => return Err(err),
|
||||||
|
|
|
@ -14,6 +14,10 @@ pub enum TokenKind {
|
||||||
Pipe,
|
Pipe,
|
||||||
/// Double quotes.
|
/// Double quotes.
|
||||||
DoubleQuote,
|
DoubleQuote,
|
||||||
|
/// Opening parenthesis.
|
||||||
|
OpeningParenthesis,
|
||||||
|
/// Closing parenthesis.
|
||||||
|
ClosingParenthesis,
|
||||||
/// Plain text. Either outside or inside of quotes.
|
/// Plain text. Either outside or inside of quotes.
|
||||||
PlainText,
|
PlainText,
|
||||||
/// A backslash followed by another character.
|
/// A backslash followed by another character.
|
||||||
|
@ -103,6 +107,8 @@ impl Lexer<'_> {
|
||||||
';' => TokenKind::Semicolon,
|
';' => TokenKind::Semicolon,
|
||||||
'|' => TokenKind::Pipe,
|
'|' => TokenKind::Pipe,
|
||||||
'"' => TokenKind::DoubleQuote,
|
'"' => TokenKind::DoubleQuote,
|
||||||
|
'(' => TokenKind::OpeningParenthesis,
|
||||||
|
')' => TokenKind::ClosingParenthesis,
|
||||||
|
|
||||||
c if is_whitespace(c) => {
|
c if is_whitespace(c) => {
|
||||||
self.eat_while(is_whitespace);
|
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
|
TokenKind::PlainText
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -147,6 +155,8 @@ impl Lexer<'_> {
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
let kind = match c {
|
let kind = match c {
|
||||||
'"' => TokenKind::DoubleQuote,
|
'"' => TokenKind::DoubleQuote,
|
||||||
|
'(' => TokenKind::OpeningParenthesis,
|
||||||
|
')' => TokenKind::ClosingParenthesis,
|
||||||
|
|
||||||
'\\' => {
|
'\\' => {
|
||||||
self.next_char();
|
self.next_char();
|
||||||
|
@ -154,7 +164,7 @@ impl Lexer<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
self.eat_while(|c| !['"', '\\'].contains(&c));
|
self.eat_while(|c| !['"', '\\', '(', ')'].contains(&c));
|
||||||
TokenKind::PlainText
|
TokenKind::PlainText
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
//! _terminator ::= SEMICOLON | NEWLINES | EOF
|
//! _terminator ::= SEMICOLON | NEWLINES | EOF
|
||||||
//! pipeline ::= command (PIPE NEWLINES? command)*
|
//! pipeline ::= command (PIPE NEWLINES? command)*
|
||||||
//! command ::= word+
|
//! 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
|
//! extras ::= COMMENT | WHITESPACE | BACKSLASH_N
|
||||||
//! ```
|
//! ```
|
||||||
|
|
|
@ -11,6 +11,8 @@ pub enum ParseError {
|
||||||
UnexpectedEof,
|
UnexpectedEof,
|
||||||
#[error("unexpected semicolon in the middle of statement")]
|
#[error("unexpected semicolon in the middle of statement")]
|
||||||
UnexpectedSemicolon,
|
UnexpectedSemicolon,
|
||||||
|
#[error("unmatched closing parenthesis")]
|
||||||
|
UnexpectedClosingParenthesis,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type of the node.
|
/// Type of the node.
|
||||||
|
@ -21,11 +23,14 @@ pub enum NodeKind {
|
||||||
Semicolon,
|
Semicolon,
|
||||||
Pipe,
|
Pipe,
|
||||||
DoubleQuote,
|
DoubleQuote,
|
||||||
|
OpeningParenthesis,
|
||||||
|
ClosingParenthesis,
|
||||||
PlainText,
|
PlainText,
|
||||||
EscapedChar,
|
EscapedChar,
|
||||||
Comment,
|
Comment,
|
||||||
|
|
||||||
Program,
|
Program,
|
||||||
|
CommandSubstitution,
|
||||||
Pipeline,
|
Pipeline,
|
||||||
Command,
|
Command,
|
||||||
Word,
|
Word,
|
||||||
|
@ -157,48 +162,76 @@ impl Iterator for Parser<'_> {
|
||||||
match self.stack.last() {
|
match self.stack.last() {
|
||||||
None => None,
|
None => None,
|
||||||
Some(nt) => match nt {
|
Some(nt) => match nt {
|
||||||
|
// XXX: unify Program and CommandSubstitution to avoid duplication
|
||||||
NodeKind::Program => match self.lookahead.kind {
|
NodeKind::Program => match self.lookahead.kind {
|
||||||
Whitespace => leaf!(Whitespace),
|
Whitespace => leaf!(Whitespace),
|
||||||
Newlines => leaf!(Newlines),
|
Newlines => leaf!(Newlines),
|
||||||
Semicolon => leaf!(Semicolon),
|
Semicolon => leaf!(Semicolon),
|
||||||
Comment => leaf!(Comment),
|
Comment => leaf!(Comment),
|
||||||
PlainText | DoubleQuote | EscapedChar => call!(Pipeline),
|
PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Pipeline),
|
||||||
Pipe => error!(UnexpectedPipe),
|
Pipe => error!(UnexpectedPipe),
|
||||||
|
ClosingParenthesis => error!(UnexpectedClosingParenthesis),
|
||||||
Eof => chain!(None, ret!()), // return silently
|
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 {
|
NodeKind::Pipeline => match self.lookahead.kind {
|
||||||
Whitespace => leaf!(Whitespace),
|
Whitespace => leaf!(Whitespace),
|
||||||
Comment => leaf!(Comment),
|
Comment => leaf!(Comment),
|
||||||
Pipe => chain!(leaf!(Pipe), call!(PipelineCont)),
|
Pipe => chain!(leaf!(Pipe), call!(PipelineCont)),
|
||||||
PlainText | DoubleQuote | EscapedChar => call!(Command),
|
PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Command),
|
||||||
Newlines | Semicolon | Eof => ret!(),
|
Newlines | Semicolon | ClosingParenthesis | Eof => ret!(),
|
||||||
},
|
},
|
||||||
NodeKind::PipelineCont => match self.lookahead.kind {
|
NodeKind::PipelineCont => match self.lookahead.kind {
|
||||||
Whitespace => leaf!(Whitespace),
|
Whitespace => leaf!(Whitespace),
|
||||||
Newlines => leaf!(Newlines),
|
Newlines => leaf!(Newlines),
|
||||||
Comment => leaf!(Comment),
|
Comment => leaf!(Comment),
|
||||||
PlainText | DoubleQuote | EscapedChar => tailcall!(Command),
|
PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => {
|
||||||
|
tailcall!(Command)
|
||||||
|
}
|
||||||
Semicolon => chain_buf!(chain!(error!(UnexpectedSemicolon), ret!()), ret!()),
|
Semicolon => chain_buf!(chain!(error!(UnexpectedSemicolon), ret!()), ret!()),
|
||||||
Pipe => chain!(error!(UnexpectedPipe), ret!()),
|
Pipe => chain!(error!(UnexpectedPipe), ret!()),
|
||||||
|
ClosingParenthesis => chain!(error!(UnexpectedClosingParenthesis), ret!()),
|
||||||
Eof => chain!(error!(UnexpectedEof), ret!()),
|
Eof => chain!(error!(UnexpectedEof), ret!()),
|
||||||
},
|
},
|
||||||
NodeKind::Command => match self.lookahead.kind {
|
NodeKind::Command => match self.lookahead.kind {
|
||||||
Whitespace => leaf!(Whitespace),
|
Whitespace => leaf!(Whitespace),
|
||||||
Comment => leaf!(Comment),
|
Comment => leaf!(Comment),
|
||||||
PlainText | DoubleQuote | EscapedChar => call!(Word),
|
PlainText | DoubleQuote | OpeningParenthesis | EscapedChar => call!(Word),
|
||||||
Newlines | Semicolon | Eof => ret!(),
|
Newlines | Semicolon | Pipe | ClosingParenthesis | Eof => ret!(),
|
||||||
Pipe => ret!(),
|
|
||||||
},
|
},
|
||||||
NodeKind::Word => match self.lookahead.kind {
|
NodeKind::Word => match self.lookahead.kind {
|
||||||
PlainText => leaf!(PlainText),
|
PlainText => leaf!(PlainText),
|
||||||
EscapedChar => leaf!(EscapedChar),
|
EscapedChar => leaf!(EscapedChar),
|
||||||
DoubleQuote => chain_buf!(call!(DQuotedString), leaf!(DoubleQuote, String)),
|
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 {
|
NodeKind::DQuotedString => match self.lookahead.kind {
|
||||||
PlainText => leaf!(PlainText, String),
|
PlainText => leaf!(PlainText, String),
|
||||||
EscapedChar => leaf!(EscapedChar, String),
|
EscapedChar => leaf!(EscapedChar, String),
|
||||||
DoubleQuote => chain_buf!(leaf!(DoubleQuote, Command), ret!()),
|
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!()),
|
Eof => chain_buf!(error!(UnexpectedEof, Command), ret!()),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -170,3 +170,22 @@ fn unterminated_double_quotes() {
|
||||||
|
|
||||||
assert_snapshot!(actual);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -170,3 +170,22 @@ fn unterminated_double_quotes() {
|
||||||
|
|
||||||
assert_snapshot!(actual);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -108,3 +108,39 @@ fn escaped_quotes_in_string() {
|
||||||
|
|
||||||
assert_snapshot!(actual);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -170,3 +170,22 @@ fn unterminated_double_quotes() {
|
||||||
|
|
||||||
assert_snapshot!(actual);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: crates/oyster_parser/tests/it/lexer.rs
|
||||||
|
expression: actual
|
||||||
|
---
|
||||||
|
Token {
|
||||||
|
kind: ClosingParenthesis,
|
||||||
|
len: 1,
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: crates/oyster_parser/tests/it/lexer.rs
|
||||||
|
expression: actual
|
||||||
|
---
|
||||||
|
Token {
|
||||||
|
kind: ClosingParenthesis,
|
||||||
|
len: 1,
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: crates/oyster_parser/tests/it/lexer.rs
|
||||||
|
expression: actual
|
||||||
|
---
|
||||||
|
Token {
|
||||||
|
kind: OpeningParenthesis,
|
||||||
|
len: 1,
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: crates/oyster_parser/tests/it/lexer.rs
|
||||||
|
expression: actual
|
||||||
|
---
|
||||||
|
Token {
|
||||||
|
kind: OpeningParenthesis,
|
||||||
|
len: 1,
|
||||||
|
}
|
|
@ -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,
|
||||||
|
]
|
|
@ -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,
|
||||||
|
]
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
use std::{borrow::Cow, collections::HashMap, ffi::OsStr};
|
||||||
|
|
||||||
use oyster_builtin_proc::builtin;
|
use oyster_builtin_proc::builtin;
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ use crate::Shell;
|
||||||
pub struct Builtin {
|
pub struct Builtin {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub description: &'static str,
|
pub description: &'static str,
|
||||||
pub fun: fn(shell: &mut Shell, args: &[Cow<str>]),
|
pub fun: fn(shell: &mut Shell, args: &[Cow<OsStr>]),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builtin(description = "prints help for different builtins")]
|
#[builtin(description = "prints help for different builtins")]
|
||||||
fn help(shell: &mut Shell, _args: &[Cow<str>]) {
|
fn help(shell: &mut Shell, _args: &[Cow<OsStr>]) {
|
||||||
println!(
|
println!(
|
||||||
r"oyster help:
|
r"oyster help:
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ These are the loaded builtins:"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to register and retrieve builtins.
|
/// Used to register and retrieve builtins.
|
||||||
|
/// Builtin names gotta be valid unicode.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct BuiltinMap(HashMap<&'static str, Builtin>);
|
pub struct BuiltinMap(HashMap<&'static str, Builtin>);
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,25 @@
|
||||||
pub mod builtins;
|
pub mod builtins;
|
||||||
mod pipeline;
|
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 builtins::BuiltinMap;
|
||||||
use nix::{
|
use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
|
fcntl::OFlag,
|
||||||
|
libc,
|
||||||
sys::{
|
sys::{
|
||||||
signal::{self, SaFlags, SigAction, SigHandler, Signal},
|
signal::{self, SaFlags, SigAction, SigHandler, Signal},
|
||||||
signalfd::SigSet,
|
signalfd::SigSet,
|
||||||
|
wait::{self, WaitPidFlag},
|
||||||
},
|
},
|
||||||
|
unistd::{self, ForkResult},
|
||||||
};
|
};
|
||||||
use oyster_parser::ast;
|
use oyster_parser::ast;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -34,11 +44,14 @@ pub enum RuntimeError {
|
||||||
#[error("failed to create pipe: {0}")]
|
#[error("failed to create pipe: {0}")]
|
||||||
PipeCreationFailed(#[source] nix::Error),
|
PipeCreationFailed(#[source] nix::Error),
|
||||||
|
|
||||||
#[error("command not found: {0}")]
|
#[error("failed to dup fd: {0}")]
|
||||||
CommandNotFound(String),
|
DupFailed(#[source] nix::Error),
|
||||||
|
|
||||||
#[error("permission denied: {0}")]
|
#[error("command not found: {0:?}")]
|
||||||
PermissionDenied(String),
|
CommandNotFound(OsString),
|
||||||
|
|
||||||
|
#[error("permission denied: {0:?}")]
|
||||||
|
PermissionDenied(OsString),
|
||||||
|
|
||||||
#[error("failed to spawn process: {0}")]
|
#[error("failed to spawn process: {0}")]
|
||||||
SpawnFailed(#[source] io::Error),
|
SpawnFailed(#[source] io::Error),
|
||||||
|
@ -48,6 +61,9 @@ pub enum RuntimeError {
|
||||||
|
|
||||||
#[error("waitpid error: {0}")]
|
#[error("waitpid error: {0}")]
|
||||||
WaidPid(nix::Error),
|
WaidPid(nix::Error),
|
||||||
|
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
IOError(#[source] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Shell {
|
pub struct Shell {
|
||||||
|
@ -79,4 +95,45 @@ impl Shell {
|
||||||
|
|
||||||
Ok(last_status)
|
Ok(last_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_subshell<'a>(&mut self, code: &'a ast::Code) -> Result<OsString, RuntimeError> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
ffi::{OsStr, OsString},
|
ffi::OsStr,
|
||||||
fs::File,
|
fs::File,
|
||||||
io,
|
io,
|
||||||
os::unix::{
|
os::unix::{
|
||||||
|
@ -34,8 +34,8 @@ enum CommandKind {
|
||||||
/// A command that's ready to be run.
|
/// A command that's ready to be run.
|
||||||
struct PreparedCommand<'a> {
|
struct PreparedCommand<'a> {
|
||||||
kind: CommandKind,
|
kind: CommandKind,
|
||||||
cmd: Cow<'a, str>,
|
cmd: Cow<'a, OsStr>,
|
||||||
args: Vec<Cow<'a, str>>,
|
args: Vec<Cow<'a, OsStr>>,
|
||||||
redirect: Redirect,
|
redirect: Redirect,
|
||||||
stdin: Option<File>,
|
stdin: Option<File>,
|
||||||
stdout: Option<File>,
|
stdout: Option<File>,
|
||||||
|
@ -44,17 +44,23 @@ struct PreparedCommand<'a> {
|
||||||
|
|
||||||
impl<'a> PreparedCommand<'a> {
|
impl<'a> PreparedCommand<'a> {
|
||||||
/// Create a new PreparedCommand.
|
/// 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 = command.0.iter().map(|word| {
|
||||||
let mut words = word.0.iter();
|
let mut words = word.0.iter();
|
||||||
|
|
||||||
let mut s = match words.next().unwrap() {
|
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 {
|
for part in words {
|
||||||
match part {
|
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 args = words.collect();
|
||||||
let redirect = command.1;
|
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),
|
Some(builtin) => CommandKind::Builtin(*builtin),
|
||||||
None => CommandKind::External,
|
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 {
|
let child = match self.kind {
|
||||||
CommandKind::External => {
|
CommandKind::External => {
|
||||||
let mut cmd = Command::new(self.cmd.as_ref());
|
let mut cmd = Command::new(&self.cmd);
|
||||||
cmd.args(args);
|
cmd.args(self.args);
|
||||||
cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from));
|
cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from));
|
||||||
cmd.stdout(self.stdout.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));
|
cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from));
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
env,
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufRead, BufReader, Write},
|
io::{BufRead, BufReader, Write},
|
||||||
os::unix::io::FromRawFd,
|
os::unix::io::FromRawFd,
|
||||||
|
@ -22,7 +23,7 @@ use oyster_runtime::Shell;
|
||||||
ioctl_write_int_bad!(tiocsctty, libc::TIOCSCTTY);
|
ioctl_write_int_bad!(tiocsctty, libc::TIOCSCTTY);
|
||||||
|
|
||||||
#[builtin(description = "test builtin")]
|
#[builtin(description = "test builtin")]
|
||||||
fn test_builtin(_: &mut Shell, _: &[Cow<str>]) {
|
fn test_builtin(_: &mut Shell, _: &[Cow<OsStr>]) {
|
||||||
// XXX: this is a workaround around libtest's use of io::set_output_capture
|
// XXX: this is a workaround around libtest's use of io::set_output_capture
|
||||||
let _ = write!(std::io::stdout(), "this is a test\n");
|
let _ = write!(std::io::stdout(), "this is a test\n");
|
||||||
}
|
}
|
||||||
|
@ -227,3 +228,34 @@ fn builtin_redirection() {
|
||||||
|
|
||||||
assert_snapshot!(actual);
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||||
|
expression: actual
|
||||||
|
---
|
||||||
|
"hello\r\n"
|
Loading…
Reference in a new issue