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:
buffet 2022-10-24 19:27:04 +00:00
parent 65b50557c9
commit 4e1a835ddc
24 changed files with 710 additions and 37 deletions

View file

@ -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>]) {}
} }

View file

@ -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),

View file

@ -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
} }
}; };

View file

@ -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
//! ``` //! ```

View file

@ -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!(),
}, },

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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,
),
],
),
),
],
),
)

View file

@ -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,
),
],
),
),
],
),
)

View file

@ -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,
},
],
},
],
},
],
},
],
},
],
}

View file

@ -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,
},
],
},
],
},
],
},
],
},
],
}

View file

@ -0,0 +1,8 @@
---
source: crates/oyster_parser/tests/it/lexer.rs
expression: actual
---
Token {
kind: ClosingParenthesis,
len: 1,
}

View file

@ -0,0 +1,8 @@
---
source: crates/oyster_parser/tests/it/lexer.rs
expression: actual
---
Token {
kind: ClosingParenthesis,
len: 1,
}

View file

@ -0,0 +1,8 @@
---
source: crates/oyster_parser/tests/it/lexer.rs
expression: actual
---
Token {
kind: OpeningParenthesis,
len: 1,
}

View file

@ -0,0 +1,8 @@
---
source: crates/oyster_parser/tests/it/lexer.rs
expression: actual
---
Token {
kind: OpeningParenthesis,
len: 1,
}

View file

@ -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,
]

View file

@ -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,
]

View file

@ -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>);

View file

@ -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);
}
}
}
} }

View file

@ -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));

View file

@ -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);
}

View file

@ -0,0 +1,5 @@
---
source: crates/oyster_runtime/tests/it/pipeline.rs
expression: actual
---
"hello\r\n"