Skip to content

Commit

Permalink
feat: support parsing fd redirects (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret authored Mar 21, 2024
1 parent 3847599 commit 32580d9
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 26 deletions.
152 changes: 127 additions & 25 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,20 @@ pub enum RedirectFd {
pub struct Redirect {
pub maybe_fd: Option<RedirectFd>,
pub op: RedirectOp,
pub io_file: Word,
pub io_file: IoFile,
}

#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
#[cfg_attr(
feature = "serialization",
serde(rename_all = "camelCase", tag = "kind", content = "value")
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IoFile {
/// Filename to redirect to/from (ex. `file.txt`` in `cmd < file.txt`)
Word(Word),
/// File descriptor to redirect to/from (ex. `2` in `cmd >&2`)
Fd(u32),
}

#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
Expand Down Expand Up @@ -564,10 +577,12 @@ fn parse_redirect(input: &str) -> ParseResult<Redirect> {
map(or(tag(">"), tag(">|")), |_| {
RedirectOp::Output(RedirectOpOutput::Overwrite)
}),
map(tag("<"), |_| RedirectOp::Input(RedirectOpInput::Redirect)),
map(ch('<'), |_| RedirectOp::Input(RedirectOpInput::Redirect)),
)(input)?;
let (input, io_file) = or(
map(preceded(ch('&'), parse_u32), IoFile::Fd),
map(preceded(skip_whitespace, parse_word), IoFile::Word),
)(input)?;
let (input, _) = skip_whitespace(input)?;
let (input, io_file) = parse_word(input)?;

let maybe_fd = if let Some(fd) = maybe_fd {
Some(RedirectFd::Fd(fd))
Expand Down Expand Up @@ -1502,7 +1517,9 @@ mod test {
redirect: Some(Redirect {
maybe_fd: None,
op: RedirectOp::Output(RedirectOpOutput::Overwrite),
io_file: Word(vec![WordPart::Text("test.txt".to_string())]),
io_file: IoFile::Word(Word(vec![WordPart::Text(
"test.txt".to_string(),
)])),
}),
});

Expand All @@ -1521,9 +1538,9 @@ mod test {
redirect: Some(Redirect {
maybe_fd: None,
op: RedirectOp::Output(RedirectOpOutput::Append),
io_file: Word(vec![WordPart::Quoted(vec![WordPart::Text(
"test.txt".to_string(),
)])]),
io_file: IoFile::Word(Word(vec![WordPart::Quoted(vec![
WordPart::Text("test.txt".to_string()),
])])),
}),
}),
);
Expand All @@ -1540,7 +1557,9 @@ mod test {
redirect: Some(Redirect {
maybe_fd: Some(RedirectFd::Fd(2)),
op: RedirectOp::Output(RedirectOpOutput::Overwrite),
io_file: Word(vec![WordPart::Text("test.txt".to_string())]),
io_file: IoFile::Word(Word(vec![WordPart::Text(
"test.txt".to_string(),
)])),
}),
}),
);
Expand All @@ -1557,7 +1576,43 @@ mod test {
redirect: Some(Redirect {
maybe_fd: Some(RedirectFd::StdoutStderr),
op: RedirectOp::Output(RedirectOpOutput::Overwrite),
io_file: Word(vec![WordPart::Text("test.txt".to_string())]),
io_file: IoFile::Word(Word(vec![WordPart::Text(
"test.txt".to_string(),
)])),
}),
}),
);

// output redirect to fd
run_test(
parse_command,
r#"command 2>&1"#,
Ok(Command {
inner: CommandInner::Simple(SimpleCommand {
env_vars: vec![],
args: vec![Word::new_word("command")],
}),
redirect: Some(Redirect {
maybe_fd: Some(RedirectFd::Fd(2)),
op: RedirectOp::Output(RedirectOpOutput::Overwrite),
io_file: IoFile::Fd(1),
}),
}),
);

// input redirect to fd
run_test(
parse_command,
r#"command <&0"#,
Ok(Command {
inner: CommandInner::Simple(SimpleCommand {
env_vars: vec![],
args: vec![Word::new_word("command")],
}),
redirect: Some(Redirect {
maybe_fd: None,
op: RedirectOp::Input(RedirectOpInput::Redirect),
io_file: IoFile::Fd(0),
}),
}),
);
Expand Down Expand Up @@ -1598,10 +1653,13 @@ mod test {
},
"kind": "command",
"redirect": {
"ioFile": [{
"kind": "text",
"value": "output.txt"
}],
"ioFile": {
"kind": "word",
"value": [{
"kind": "text",
"value": "output.txt"
}],
},
"maybeFd": null,
"op": {
"kind": "output",
Expand Down Expand Up @@ -1632,10 +1690,13 @@ mod test {
},
"kind": "command",
"redirect": {
"ioFile": [{
"kind": "text",
"value": "output.txt"
}],
"ioFile": {
"kind": "word",
"value": [{
"kind": "text",
"value": "output.txt"
}],
},
"maybeFd": {
"kind": "fd",
"fd": 2,
Expand Down Expand Up @@ -1669,10 +1730,13 @@ mod test {
},
"kind": "command",
"redirect": {
"ioFile": [{
"kind": "text",
"value": "output.txt"
}],
"ioFile": {
"kind": "word",
"value": [{
"kind": "text",
"value": "output.txt"
}],
},
"maybeFd": {
"kind": "stdoutStderr"
},
Expand Down Expand Up @@ -1705,10 +1769,48 @@ mod test {
},
"kind": "command",
"redirect": {
"ioFile": [{
"ioFile": {
"kind": "word",
"value": [{
"kind": "text",
"value": "output.txt"
}],
},
"maybeFd": null,
"op": {
"kind": "input",
"value": "redirect",
}
}
},
"kind": "pipeline",
"negated": false
}
}]
}),
);

assert_json_equals(
serialize_to_json("./example <&0"),
serde_json::json!({
"items": [{
"isAsync": false,
"sequence": {
"inner": {
"inner": {
"args": [[{
"kind": "text",
"value": "output.txt"
}],
"value": "./example"
}]],
"envVars": [],
"kind": "simple"
},
"kind": "command",
"redirect": {
"ioFile": {
"kind": "fd",
"value": 0,
},
"maybeFd": null,
"op": {
"kind": "input",
Expand Down
12 changes: 11 additions & 1 deletion src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use futures::FutureExt;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;

use crate::parser::IoFile;
use crate::parser::RedirectOpInput;
use crate::parser::RedirectOpOutput;
pub use crate::shell::commands::ExecutableCommand;
Expand Down Expand Up @@ -377,8 +378,17 @@ async fn resolve_redirect_pipe(
}
}

let word = match redirect.io_file.clone() {
IoFile::Word(word) => word,
IoFile::Fd(_) => {
let _ = stderr.write_line(
"deno_task_shell: redirecting file descriptors is not implemented",
);
return Err(ExecuteResult::from_exit_code(1));
}
};
let words = evaluate_word_parts(
redirect.io_file.clone().into_parts(),
word.into_parts(),
state,
stdin.clone(),
stderr.clone(),
Expand Down
18 changes: 18 additions & 0 deletions src/shell/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,15 @@ async fn redirects_output() {
.assert_exit_code(1)
.run()
.await;

TestBuilder::new()
.command(r#"echo 1 >&2"#)
.assert_stderr(
"deno_task_shell: redirecting file descriptors is not implemented\n",
)
.assert_exit_code(1)
.run()
.await;
}

#[tokio::test]
Expand All @@ -603,6 +612,15 @@ async fn redirects_input() {
.assert_stdout("Hi!\nThere\n")
.run()
.await;

TestBuilder::new()
.command(r#"cat - <&0"#)
.assert_stderr(
"deno_task_shell: redirecting file descriptors is not implemented\n",
)
.assert_exit_code(1)
.run()
.await;
}

#[tokio::test]
Expand Down

0 comments on commit 32580d9

Please sign in to comment.