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_runtime::Shell;
|
||||
|
@ -6,5 +6,5 @@ use oyster_runtime::Shell;
|
|||
#[test]
|
||||
fn normal_usage() {
|
||||
#[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)]
|
||||
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<WordPart<'a>>,
|
||||
) -> 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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
//! ```
|
||||
|
|
|
@ -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!(),
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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<str>]),
|
||||
pub fun: fn(shell: &mut Shell, args: &[Cow<OsStr>]),
|
||||
}
|
||||
|
||||
#[builtin(description = "prints help for different builtins")]
|
||||
fn help(shell: &mut Shell, _args: &[Cow<str>]) {
|
||||
fn help(shell: &mut Shell, _args: &[Cow<OsStr>]) {
|
||||
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>);
|
||||
|
||||
|
|
|
@ -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<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::{
|
||||
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<Cow<'a, str>>,
|
||||
cmd: Cow<'a, OsStr>,
|
||||
args: Vec<Cow<'a, OsStr>>,
|
||||
redirect: Redirect,
|
||||
stdin: Option<File>,
|
||||
stdout: Option<File>,
|
||||
|
@ -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));
|
||||
|
|
|
@ -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<str>]) {
|
||||
fn test_builtin(_: &mut Shell, _: &[Cow<OsStr>]) {
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
"hello\r\n"
|
Loading…
Reference in a new issue