diff --git a/src/parser/pipelines/collector.rs b/src/parser/pipelines/collector.rs index 0fb6c6d624bb501d3f0f63b029ee06b414e742d3..1b047844b1c19889a3be261170223709ccf0cb98 100644 --- a/src/parser/pipelines/collector.rs +++ b/src/parser/pipelines/collector.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use std::iter::Peekable; -use super::{Input, Pipeline, RedirectFrom, Redirection}; +use super::{Input, PipeItem, Pipeline, RedirectFrom, Redirection}; use shell::{Job, JobKind}; use types::*; @@ -200,48 +200,58 @@ impl<'a> Collector<'a> { pub(crate) fn parse(&self) -> Result<Pipeline, &'static str> { let mut bytes = self.data.bytes().enumerate().peekable(); let mut args = Array::new(); - let mut jobs: Vec<Job> = Vec::new(); - let mut input: Option<Input> = None; - let mut outfile: Option<Redirection> = None; + let mut pipeline = Pipeline::new(); + let mut outputs: Option<Vec<Redirection>> = None; + let mut inputs: Option<Vec<Input>> = None; - /// Attempt to create a new job given a list of collected arguments - macro_rules! try_add_job { - ($kind:expr) => {{ - if ! args.is_empty() { - jobs.push(Job::new(args.clone(), $kind)); - args.clear(); + /// Add a new argument that is re + macro_rules! push_arg { + () => {{ + if let Some(v) = self.arg(&mut bytes)? { + args.push(v.into()); } }} } - /// Attempt to create a job that redirects to some output file + /// Attempt to add a redirection macro_rules! try_redir_out { ($from:expr) => {{ - try_add_job!(JobKind::Last); + if let None = outputs { outputs = Some(Vec::new()); } let append = if let Some(&(_, b'>')) = bytes.peek() { - // Consume the next byte if it is part of the redirection bytes.next(); true } else { false }; if let Some(file) = self.arg(&mut bytes)? { - outfile = Some(Redirection { + outputs.as_mut().map(|o| o.push(Redirection { from: $from, file: file.into(), append - }); + })); } else { return Err("expected file argument after redirection for output"); } }} - } + }; - /// Add a new argument that is re - macro_rules! push_arg { - () => {{ - if let Some(v) = self.arg(&mut bytes)? { - args.push(v.into()); + /// Attempt to create a pipeitem and append it to the pipeline + macro_rules! try_add_item { + ($job_kind:expr) => {{ + if ! args.is_empty() { + let job = Job::new(args.clone(), $job_kind); + args.clear(); + let item_out = if let Some(out_tmp) = outputs.take() { + out_tmp + } else { + Vec::new() + }; + let item_in = if let Some(in_tmp) = inputs.take() { + in_tmp + } else { + Vec::new() + }; + pipeline.items.push(PipeItem::new(job, item_out, item_in)); } }} } @@ -260,14 +270,14 @@ impl<'a> Collector<'a> { } Some(&(_, b'|')) => { bytes.next(); - try_add_job!(JobKind::Pipe(RedirectFrom::Both)); + try_add_item!(JobKind::Pipe(RedirectFrom::Both)); } Some(&(_, b'&')) => { bytes.next(); - try_add_job!(JobKind::And); + try_add_item!(JobKind::And); } Some(_) | None => { - try_add_job!(JobKind::Background); + try_add_item!(JobKind::Background); } } } @@ -283,7 +293,7 @@ impl<'a> Collector<'a> { Some(b'|') => { bytes.next(); bytes.next(); - try_add_job!(JobKind::Pipe(RedirectFrom::Stderr)); + try_add_item!(JobKind::Pipe(RedirectFrom::Stderr)); } Some(_) | None => push_arg!(), } @@ -293,10 +303,10 @@ impl<'a> Collector<'a> { match bytes.peek() { Some(&(_, b'|')) => { bytes.next(); - try_add_job!(JobKind::Or); + try_add_item!(JobKind::Or); } Some(_) | None => { - try_add_job!(JobKind::Pipe(RedirectFrom::Stdout)); + try_add_item!(JobKind::Pipe(RedirectFrom::Stdout)); } } } @@ -305,6 +315,7 @@ impl<'a> Collector<'a> { try_redir_out!(RedirectFrom::Stdout); } b'<' => { + if let None = inputs { inputs = Some(Vec::new()); } bytes.next(); if Some(b'<') == self.peek(i + 1) { if Some(b'<') == self.peek(i + 2) { @@ -313,7 +324,7 @@ impl<'a> Collector<'a> { bytes.next(); bytes.next(); if let Some(cmd) = self.arg(&mut bytes)? { - input = Some(Input::HereString(cmd.into())); + inputs.as_mut().map(|x| x.push(Input::HereString(cmd.into()))); } else { return Err("expected string argument after '<<<'"); } @@ -332,12 +343,12 @@ impl<'a> Collector<'a> { }; let heredoc = heredoc.lines().collect::<Vec<&str>>(); // Then collect the heredoc from standard input. - input = - Some(Input::HereString(heredoc[1..heredoc.len() - 1].join("\n"))); + inputs.as_mut().map(|x| + x.push(Input::HereString(heredoc[1..heredoc.len() - 1].join("\n")))); } } else if let Some(file) = self.arg(&mut bytes)? { // Otherwise interpret it as stdin redirection - input = Some(Input::File(file.into())); + inputs.as_mut().map(|x| x.push(Input::File(file.into()))); } else { return Err("expected file argument after redirection for input"); } @@ -352,16 +363,16 @@ impl<'a> Collector<'a> { } if !args.is_empty() { - jobs.push(Job::new(args, JobKind::Last)); + try_add_item!(JobKind::Last); } - Ok(Pipeline::new(jobs, input, outfile)) + Ok(pipeline) } } #[cfg(test)] mod tests { - use parser::pipelines::{Input, Pipeline, RedirectFrom, Redirection}; + use parser::pipelines::{Input, PipeItem, Pipeline, RedirectFrom, Redirection}; use parser::statement::parse; use shell::{Job, JobKind}; use shell::flow_control::Statement; @@ -371,18 +382,18 @@ mod tests { fn stderr_redirection() { if let Statement::Pipeline(pipeline) = parse("git rev-parse --abbrev-ref HEAD ^> /dev/null") { - assert_eq!("git", pipeline.jobs[0].args[0]); - assert_eq!("rev-parse", pipeline.jobs[0].args[1]); - assert_eq!("--abbrev-ref", pipeline.jobs[0].args[2]); - assert_eq!("HEAD", pipeline.jobs[0].args[3]); + assert_eq!("git", pipeline.items[0].job.args[0]); + assert_eq!("rev-parse", pipeline.items[0].job.args[1]); + assert_eq!("--abbrev-ref", pipeline.items[0].job.args[2]); + assert_eq!("HEAD", pipeline.items[0].job.args[3]); - let expected = Redirection { + let expected = vec![Redirection { from: RedirectFrom::Stderr, file: "/dev/null".to_owned(), append: false, - }; + }]; - assert_eq!(Some(expected), pipeline.stdout); + assert_eq!(expected, pipeline.items[0].outputs); } else { assert!(false); } @@ -391,9 +402,9 @@ mod tests { #[test] fn braces() { if let Statement::Pipeline(pipeline) = parse("echo {a b} {a {b c}}") { - let jobs = pipeline.jobs; - assert_eq!("{a b}", jobs[0].args[1]); - assert_eq!("{a {b c}}", jobs[0].args[2]); + let items = pipeline.items; + assert_eq!("{a b}", items[0].job.args[1]); + assert_eq!("{a {b c}}", items[0].job.args[2]); } else { assert!(false); } @@ -402,10 +413,10 @@ mod tests { #[test] fn methods() { if let Statement::Pipeline(pipeline) = parse("echo @split(var, ', ') $join(array, ',')") { - let jobs = pipeline.jobs; - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("@split(var, ', ')", jobs[0].args[1]); - assert_eq!("$join(array, ',')", jobs[0].args[2]); + let items = pipeline.items; + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("@split(var, ', ')", items[0].job.args[1]); + assert_eq!("$join(array, ',')", items[0].job.args[2]); } else { assert!(false); } @@ -414,9 +425,9 @@ mod tests { #[test] fn nested_process() { if let Statement::Pipeline(pipeline) = parse("echo $(echo one $(echo two) three)") { - let jobs = pipeline.jobs; - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("$(echo one $(echo two) three)", jobs[0].args[1]); + let items = pipeline.items; + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("$(echo one $(echo two) three)", items[0].job.args[1]); } else { assert!(false); } @@ -425,9 +436,9 @@ mod tests { #[test] fn nested_array_process() { if let Statement::Pipeline(pipeline) = parse("echo @(echo one @(echo two) three)") { - let jobs = pipeline.jobs; - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("@(echo one @(echo two) three)", jobs[0].args[1]); + let items = pipeline.items; + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("@(echo one @(echo two) three)", items[0].job.args[1]); } else { assert!(false); } @@ -436,10 +447,10 @@ mod tests { #[test] fn quoted_process() { if let Statement::Pipeline(pipeline) = parse("echo \"$(seq 1 10)\"") { - let jobs = pipeline.jobs; - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("\"$(seq 1 10)\"", jobs[0].args[1]); - assert_eq!(2, jobs[0].args.len()); + let items = pipeline.items; + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("\"$(seq 1 10)\"", items[0].job.args[1]); + assert_eq!(2, items[0].job.args.len()); } else { assert!(false); } @@ -448,10 +459,10 @@ mod tests { #[test] fn process() { if let Statement::Pipeline(pipeline) = parse("echo $(seq 1 10 | head -1)") { - let jobs = pipeline.jobs; - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("$(seq 1 10 | head -1)", jobs[0].args[1]); - assert_eq!(2, jobs[0].args.len()); + let items = pipeline.items; + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("$(seq 1 10 | head -1)", items[0].job.args[1]); + assert_eq!(2, items[0].job.args.len()); } else { assert!(false); } @@ -460,10 +471,10 @@ mod tests { #[test] fn array_process() { if let Statement::Pipeline(pipeline) = parse("echo @(seq 1 10 | head -1)") { - let jobs = pipeline.jobs; - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("@(seq 1 10 | head -1)", jobs[0].args[1]); - assert_eq!(2, jobs[0].args.len()); + let items = pipeline.items; + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("@(seq 1 10 | head -1)", items[0].job.args[1]); + assert_eq!(2, items[0].job.args.len()); } else { assert!(false); } @@ -472,10 +483,10 @@ mod tests { #[test] fn single_job_no_args() { if let Statement::Pipeline(pipeline) = parse("cat") { - let jobs = pipeline.jobs; - assert_eq!(1, jobs.len()); - assert_eq!("cat", jobs[0].command); - assert_eq!(1, jobs[0].args.len()); + let items = pipeline.items; + assert_eq!(1, items.len()); + assert_eq!("cat", items[0].job.command); + assert_eq!(1, items[0].job.args.len()); } else { assert!(false); } @@ -484,13 +495,13 @@ mod tests { #[test] fn single_job_with_single_character_arguments() { if let Statement::Pipeline(pipeline) = parse("echo a b c") { - let jobs = pipeline.jobs; - assert_eq!(1, jobs.len()); - assert_eq!("echo", jobs[0].args[0]); - assert_eq!("a", jobs[0].args[1]); - assert_eq!("b", jobs[0].args[2]); - assert_eq!("c", jobs[0].args[3]); - assert_eq!(4, jobs[0].args.len()); + let items = pipeline.items; + assert_eq!(1, items.len()); + assert_eq!("echo", items[0].job.args[0]); + assert_eq!("a", items[0].job.args[1]); + assert_eq!("b", items[0].job.args[2]); + assert_eq!("c", items[0].job.args[3]); + assert_eq!(4, items[0].job.args.len()); } else { assert!(false); } @@ -499,11 +510,11 @@ mod tests { #[test] fn job_with_args() { if let Statement::Pipeline(pipeline) = parse("ls -al dir") { - let jobs = pipeline.jobs; - assert_eq!(1, jobs.len()); - assert_eq!("ls", jobs[0].command); - assert_eq!("-al", jobs[0].args[1]); - assert_eq!("dir", jobs[0].args[2]); + let items = pipeline.items; + assert_eq!(1, items.len()); + assert_eq!("ls", items[0].job.command); + assert_eq!("-al", items[0].job.args[1]); + assert_eq!("dir", items[0].job.args[2]); } else { assert!(false); } @@ -521,11 +532,11 @@ mod tests { #[test] fn multiple_white_space_between_words() { if let Statement::Pipeline(pipeline) = parse("ls \t -al\t\tdir") { - let jobs = pipeline.jobs; - assert_eq!(1, jobs.len()); - assert_eq!("ls", jobs[0].command); - assert_eq!("-al", jobs[0].args[1]); - assert_eq!("dir", jobs[0].args[2]); + let items = pipeline.items; + assert_eq!(1, items.len()); + assert_eq!("ls", items[0].job.command); + assert_eq!("-al", items[0].job.args[1]); + assert_eq!("dir", items[0].job.args[2]); } else { assert!(false); } @@ -534,9 +545,9 @@ mod tests { #[test] fn trailing_whitespace() { if let Statement::Pipeline(pipeline) = parse("ls -al\t ") { - assert_eq!(1, pipeline.jobs.len()); - assert_eq!("ls", pipeline.jobs[0].command); - assert_eq!("-al", pipeline.jobs[0].args[1]); + assert_eq!(1, pipeline.items.len()); + assert_eq!("ls", pipeline.items[0].job.command); + assert_eq!("-al", pipeline.items[0].job.args[1]); } else { assert!(false); } @@ -545,10 +556,10 @@ mod tests { #[test] fn double_quoting() { if let Statement::Pipeline(pipeline) = parse("echo \"a > 10\" \"a < 10\"") { - let jobs = pipeline.jobs; - assert_eq!("\"a > 10\"", jobs[0].args[1]); - assert_eq!("\"a < 10\"", jobs[0].args[2]); - assert_eq!(3, jobs[0].args.len()); + let items = pipeline.items; + assert_eq!("\"a > 10\"", items[0].job.args[1]); + assert_eq!("\"a < 10\"", items[0].job.args[2]); + assert_eq!(3, items[0].job.args.len()); } else { assert!(false) } @@ -557,9 +568,9 @@ mod tests { #[test] fn double_quoting_contains_single() { if let Statement::Pipeline(pipeline) = parse("echo \"Hello 'Rusty' World\"") { - let jobs = pipeline.jobs; - assert_eq!(2, jobs[0].args.len()); - assert_eq!("\"Hello \'Rusty\' World\"", jobs[0].args[1]); + let items = pipeline.items; + assert_eq!(2, items[0].job.args.len()); + assert_eq!("\"Hello \'Rusty\' World\"", items[0].job.args[1]); } else { assert!(false) } @@ -568,17 +579,17 @@ mod tests { #[test] fn multi_quotes() { if let Statement::Pipeline(pipeline) = parse("echo \"Hello \"Rusty\" World\"") { - let jobs = pipeline.jobs; - assert_eq!(2, jobs[0].args.len()); - assert_eq!("\"Hello \"Rusty\" World\"", jobs[0].args[1]); + let items = pipeline.items; + assert_eq!(2, items[0].job.args.len()); + assert_eq!("\"Hello \"Rusty\" World\"", items[0].job.args[1]); } else { assert!(false) } if let Statement::Pipeline(pipeline) = parse("echo \'Hello \'Rusty\' World\'") { - let jobs = pipeline.jobs; - assert_eq!(2, jobs[0].args.len()); - assert_eq!("\'Hello \'Rusty\' World\'", jobs[0].args[1]); + let items = pipeline.items; + assert_eq!(2, items[0].job.args.len()); + assert_eq!("\'Hello \'Rusty\' World\'", items[0].job.args[1]); } else { assert!(false) } @@ -596,8 +607,8 @@ mod tests { #[test] fn not_background_job() { if let Statement::Pipeline(pipeline) = parse("echo hello world") { - let jobs = pipeline.jobs; - assert_eq!(JobKind::Last, jobs[0].kind); + let items = pipeline.items; + assert_eq!(JobKind::Last, items[0].job.kind); } else { assert!(false); } @@ -606,15 +617,15 @@ mod tests { #[test] fn background_job() { if let Statement::Pipeline(pipeline) = parse("echo hello world&") { - let jobs = pipeline.jobs; - assert_eq!(JobKind::Background, jobs[0].kind); + let items = pipeline.items; + assert_eq!(JobKind::Background, items[0].job.kind); } else { assert!(false); } if let Statement::Pipeline(pipeline) = parse("echo hello world &") { - let jobs = pipeline.jobs; - assert_eq!(JobKind::Background, jobs[0].kind); + let items = pipeline.items; + assert_eq!(JobKind::Background, items[0].job.kind); } else { assert!(false); } @@ -623,10 +634,10 @@ mod tests { #[test] fn and_job() { if let Statement::Pipeline(pipeline) = parse("echo one && echo two") { - let jobs = pipeline.jobs; - assert_eq!(JobKind::And, jobs[0].kind); - assert_eq!(array!["echo", "one"], jobs[0].args); - assert_eq!(array!["echo", "two"], jobs[1].args); + let items = pipeline.items; + assert_eq!(JobKind::And, items[0].job.kind); + assert_eq!(array!["echo", "one"], items[0].job.args); + assert_eq!(array!["echo", "two"], items[1].job.args); } else { assert!(false); } @@ -635,8 +646,10 @@ mod tests { #[test] fn or_job() { if let Statement::Pipeline(pipeline) = parse("echo one || echo two") { - let jobs = pipeline.jobs; - assert_eq!(JobKind::Or, jobs[0].kind); + let items = pipeline.items; + assert_eq!(JobKind::Or, items[0].job.kind); + assert_eq!(array!["echo", "one"], items[0].job.args); + assert_eq!(array!["echo", "two"], items[1].job.args); } else { assert!(false); } @@ -654,9 +667,9 @@ mod tests { #[test] fn leading_whitespace() { if let Statement::Pipeline(pipeline) = parse(" \techo") { - let jobs = pipeline.jobs; - assert_eq!(1, jobs.len()); - assert_eq!("echo", jobs[0].command); + let items = pipeline.items; + assert_eq!(1, items.len()); + assert_eq!("echo", items[0].job.command); } else { assert!(false); } @@ -665,8 +678,8 @@ mod tests { #[test] fn single_quoting() { if let Statement::Pipeline(pipeline) = parse("echo '#!!;\"\\'") { - let jobs = pipeline.jobs; - assert_eq!("'#!!;\"\\'", jobs[0].args[1]); + let items = pipeline.items; + assert_eq!("'#!!;\"\\'", items[0].job.args[1]); } else { assert!(false); } @@ -677,12 +690,12 @@ mod tests { if let Statement::Pipeline(pipeline) = parse("echo 123 456 \"ABC 'DEF' GHI\" 789 one' 'two") { - let jobs = pipeline.jobs; - assert_eq!("123", jobs[0].args[1]); - assert_eq!("456", jobs[0].args[2]); - assert_eq!("\"ABC 'DEF' GHI\"", jobs[0].args[3]); - assert_eq!("789", jobs[0].args[4]); - assert_eq!("one' 'two", jobs[0].args[5]); + let items = pipeline.items; + assert_eq!("123", items[0].job.args[1]); + assert_eq!("456", items[0].job.args[2]); + assert_eq!("\"ABC 'DEF' GHI\"", items[0].job.args[3]); + assert_eq!("789", items[0].job.args[4]); + assert_eq!("one' 'two", items[0].job.args[5]); } else { assert!(false); } @@ -698,17 +711,19 @@ mod tests { } #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. fn pipeline_with_redirection() { let input = "cat | echo hello | cat < stuff > other"; if let Statement::Pipeline(pipeline) = parse(input) { - assert_eq!(3, pipeline.jobs.len()); - assert_eq!("cat", &pipeline.clone().jobs[0].args[0]); - assert_eq!("echo", &pipeline.clone().jobs[1].args[0]); - assert_eq!("hello", &pipeline.clone().jobs[1].args[1]); - assert_eq!("cat", &pipeline.clone().jobs[2].args[0]); - assert_eq!(Some(Input::File("stuff".into())), pipeline.stdin); - assert_eq!("other", &pipeline.clone().stdout.unwrap().file); - assert!(!pipeline.clone().stdout.unwrap().append); + assert_eq!(3, pipeline.items.len()); + assert_eq!("cat", &pipeline.clone().items[0].job.args[0]); + assert_eq!("echo", &pipeline.clone().items[1].job.args[0]); + assert_eq!("hello", &pipeline.clone().items[1].job.args[1]); + assert_eq!("cat", &pipeline.clone().items[2].job.args[0]); + assert_eq!(vec![Input::File("stuff".into())], pipeline.items[2].inputs); + assert_eq!("other", &pipeline.clone().items[2].outputs[0].file); + assert!(!pipeline.clone().items[2].outputs[0].append); assert_eq!(input.to_owned(), pipeline.to_string()); } else { assert!(false); @@ -716,61 +731,124 @@ mod tests { } #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. fn pipeline_with_redirection_append() { if let Statement::Pipeline(pipeline) = parse("cat | echo hello | cat < stuff >> other") { - assert_eq!(3, pipeline.jobs.len()); - assert_eq!(Some(Input::File("stuff".into())), pipeline.stdin); - assert_eq!("other", &pipeline.clone().stdout.unwrap().file); - assert!(pipeline.clone().stdout.unwrap().append); + assert_eq!(3, pipeline.items.len()); + assert_eq!(Input::File("stuff".into()), pipeline.items[2].inputs[0]); + assert_eq!("other", pipeline.items[2].outputs[0].file); + assert!(pipeline.items[2].outputs[0].append); } else { assert!(false); } } #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. + fn multiple_redirect() { + let input = "cat < file1 <<< \"herestring\" | tr 'x' 'y' ^>> err &> both > out"; + let expected = Pipeline { items: vec![ + PipeItem { + job: Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), + inputs: vec![ + Input::File("file1".into()), + Input::HereString("\"herestring\"".into()), + ], + outputs: Vec::new(), + }, + PipeItem { + job: Job::new(array!["tr","'x'","'y'"], JobKind::Last), + inputs: Vec::new(), + outputs: vec![ + Redirection { + from: RedirectFrom::Stderr, + file: "err".into(), + append: true, + }, + Redirection { + from: RedirectFrom::Both, + file: "both".into(), + append: false, + }, + Redirection { + from: RedirectFrom::Stdout, + file: "out".into(), + append: false, + }, + ] + } + ]}; + assert_eq!(parse(input), Statement::Pipeline(expected)); + } + + #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. fn pipeline_with_redirection_append_stderr() { let input = "cat | echo hello | cat < stuff ^>> other"; - let expected = Pipeline { - jobs: vec![ - Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), - Job::new(array!["echo", "hello"], JobKind::Pipe(RedirectFrom::Stdout)), - Job::new(array!["cat"], JobKind::Last), - ], - stdin: Some(Input::File("stuff".into())), - stdout: Some(Redirection { - from: RedirectFrom::Stderr, - file: "other".into(), - append: true, - }), - }; + let expected = Pipeline { items: vec![ + PipeItem { + job: Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), + inputs: Vec::new(), + outputs: Vec::new(), + }, + PipeItem { + job: Job::new(array!["echo", "hello"], JobKind::Pipe(RedirectFrom::Stdout)), + inputs: Vec::new(), + outputs: Vec::new(), + }, + PipeItem { + job: Job::new(array!["cat"], JobKind::Last), + inputs: vec![Input::File("stuff".into())], + outputs: vec![Redirection { + from: RedirectFrom::Stderr, + file: "other".into(), + append: true, + }], + }, + ]}; assert_eq!(parse(input), Statement::Pipeline(expected)); } #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. fn pipeline_with_redirection_append_both() { let input = "cat | echo hello | cat < stuff &>> other"; - let expected = Pipeline { - jobs: vec![ - Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), - Job::new(array!["echo", "hello"], JobKind::Pipe(RedirectFrom::Stdout)), - Job::new(array!["cat"], JobKind::Last), - ], - stdin: Some(Input::File("stuff".into())), - stdout: Some(Redirection { - from: RedirectFrom::Both, - file: "other".into(), - append: true, - }), - }; + let expected = Pipeline { items: vec![ + PipeItem { + job: Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), + inputs: Vec::new(), + outputs: Vec::new(), + }, + PipeItem { + job: Job::new(array!["echo", "hello"], JobKind::Pipe(RedirectFrom::Stdout)), + inputs: Vec::new(), + outputs: Vec::new(), + }, + PipeItem { + job: Job::new(array!["cat"], JobKind::Last), + inputs: vec![Input::File("stuff".into())], + outputs: vec![Redirection { + from: RedirectFrom::Both, + file: "other".into(), + append: true, + }], + }, + ]}; assert_eq!(parse(input), Statement::Pipeline(expected)); } #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. fn pipeline_with_redirection_reverse_order() { if let Statement::Pipeline(pipeline) = parse("cat | echo hello | cat > stuff < other") { - assert_eq!(3, pipeline.jobs.len()); - assert_eq!(Some(Input::File("other".into())), pipeline.stdin); - assert_eq!("stuff", &pipeline.clone().stdout.unwrap().file); + assert_eq!(3, pipeline.items.len()); + assert_eq!(vec![Input::File("other".into())], pipeline.items[2].inputs); + assert_eq!("stuff", pipeline.items[2].outputs[0].file); } else { assert!(false); } @@ -779,20 +857,20 @@ mod tests { #[test] fn var_meets_quote() { if let Statement::Pipeline(pipeline) = parse("echo $x '{()}' test") { - assert_eq!(1, pipeline.jobs.len()); - assert_eq!("echo", &pipeline.clone().jobs[0].args[0]); - assert_eq!("$x", &pipeline.clone().jobs[0].args[1]); - assert_eq!("'{()}'", &pipeline.clone().jobs[0].args[2]); - assert_eq!("test", &pipeline.clone().jobs[0].args[3]); + assert_eq!(1, pipeline.items.len()); + assert_eq!("echo", &pipeline.clone().items[0].job.args[0]); + assert_eq!("$x", &pipeline.clone().items[0].job.args[1]); + assert_eq!("'{()}'", &pipeline.clone().items[0].job.args[2]); + assert_eq!("test", &pipeline.clone().items[0].job.args[3]); } else { assert!(false); } if let Statement::Pipeline(pipeline) = parse("echo $x'{()}' test") { - assert_eq!(1, pipeline.jobs.len()); - assert_eq!("echo", &pipeline.clone().jobs[0].args[0]); - assert_eq!("$x'{()}'", &pipeline.clone().jobs[0].args[1]); - assert_eq!("test", &pipeline.clone().jobs[0].args[2]); + assert_eq!(1, pipeline.items.len()); + assert_eq!("echo", &pipeline.clone().items[0].job.args[0]); + assert_eq!("$x'{()}'", &pipeline.clone().items[0].job.args[1]); + assert_eq!("test", &pipeline.clone().items[0].job.args[2]); } else { assert!(false); } @@ -802,9 +880,11 @@ mod tests { fn herestring() { let input = "calc <<< $(cat math.txt)"; let expected = Pipeline { - jobs: vec![Job::new(array!["calc"], JobKind::Last)], - stdin: Some(Input::HereString("$(cat math.txt)".into())), - stdout: None, + items: vec![PipeItem { + job: Job::new(array!["calc"], JobKind::Last), + inputs: vec![Input::HereString("$(cat math.txt)".into())], + outputs: vec![], + }] }; assert_eq!(Statement::Pipeline(expected), parse(input)); } @@ -813,40 +893,48 @@ mod tests { fn heredoc() { let input = "calc << EOF\n1 + 2\n3 + 4\nEOF"; let expected = Pipeline { - jobs: vec![Job::new(array!["calc"], JobKind::Last)], - stdin: Some(Input::HereString("1 + 2\n3 + 4".into())), - stdout: None, + items: vec![PipeItem { + job: Job::new(array!["calc"], JobKind::Last), + inputs: vec![Input::HereString("1 + 2\n3 + 4".into())], + outputs: vec![], + }] }; assert_eq!(Statement::Pipeline(expected), parse(input)); } #[test] + // FIXME: May need updating after resolution of which part of the pipe + // the input redirection shoud be associated with. fn piped_herestring() { let input = "cat | tr 'o' 'x' <<< $VAR > out.log"; - let expected = Pipeline { - jobs: vec![ - Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), - Job::new(array!["tr", "'o'", "'x'"], JobKind::Last), - ], - stdin: Some(Input::HereString("$VAR".into())), - stdout: Some(Redirection { - from: RedirectFrom::Stdout, - file: "out.log".into(), - append: false, - }), - }; + let expected = Pipeline { items: vec![ + PipeItem { + job: Job::new(array!["cat"], JobKind::Pipe(RedirectFrom::Stdout)), + inputs: Vec::new(), + outputs: Vec::new(), + }, + PipeItem { + job: Job::new(array!["tr", "'o'", "'x'"], JobKind::Last), + inputs: vec![Input::HereString("$VAR".into())], + outputs: vec![Redirection { + from: RedirectFrom::Stdout, + file: "out.log".into(), + append: false, + }], + } + ]}; assert_eq!(Statement::Pipeline(expected), parse(input)); } #[test] fn awk_tests() { if let Statement::Pipeline(pipeline) = parse("awk -v x=$x '{ if (1) print $1 }' myfile") { - assert_eq!(1, pipeline.jobs.len()); - assert_eq!("awk", &pipeline.clone().jobs[0].args[0]); - assert_eq!("-v", &pipeline.clone().jobs[0].args[1]); - assert_eq!("x=$x", &pipeline.clone().jobs[0].args[2]); - assert_eq!("'{ if (1) print $1 }'", &pipeline.clone().jobs[0].args[3]); - assert_eq!("myfile", &pipeline.clone().jobs[0].args[4]); + assert_eq!(1, pipeline.items.len()); + assert_eq!("awk", &pipeline.clone().items[0].job.args[0]); + assert_eq!("-v", &pipeline.clone().items[0].job.args[1]); + assert_eq!("x=$x", &pipeline.clone().items[0].job.args[2]); + assert_eq!("'{ if (1) print $1 }'", &pipeline.clone().items[0].job.args[3]); + assert_eq!("myfile", &pipeline.clone().items[0].job.args[4]); } else { assert!(false); } @@ -856,13 +944,17 @@ mod tests { fn escaped_filenames() { let input = "echo zardoz >> foo\\'bar"; let expected = Pipeline { - jobs: vec![Job::new(array!["echo", "zardoz"], JobKind::Last)], - stdin: None, - stdout: Some(Redirection { - from: RedirectFrom::Stdout, - file: "foo\\'bar".into(), - append: true, - }), + items: vec![ + PipeItem { + job: Job::new(array!["echo", "zardoz"], JobKind::Last), + inputs: Vec::new(), + outputs: vec![Redirection { + from: RedirectFrom::Stdout, + file: "foo\\'bar".into(), + append: true, + }], + } + ], }; assert_eq!(parse(input), Statement::Pipeline(expected)); } diff --git a/src/parser/pipelines/mod.rs b/src/parser/pipelines/mod.rs index ae5deca32dee1e0cc0d931c7b984656260782aa0..f5648d464f48c090c1e094e7361db50252631b1b 100644 --- a/src/parser/pipelines/mod.rs +++ b/src/parser/pipelines/mod.rs @@ -20,6 +20,7 @@ pub(crate) struct Redirection { pub append: bool, } + /// Represents input that a process could initially receive from `stdin` #[derive(Debug, PartialEq, Clone)] pub(crate) enum Input { @@ -32,54 +33,97 @@ pub(crate) enum Input { #[derive(Debug, PartialEq, Clone)] pub(crate) struct Pipeline { - pub jobs: Vec<Job>, - pub stdout: Option<Redirection>, - pub stdin: Option<Input>, + pub items: Vec<PipeItem>, } -impl Pipeline { - pub(crate) fn new(jobs: Vec<Job>, stdin: Option<Input>, stdout: Option<Redirection>) -> Self { - Pipeline { - jobs, - stdin, - stdout, +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct PipeItem { + pub job: Job, + pub outputs: Vec<Redirection>, + pub inputs: Vec<Input>, +} + +impl PipeItem { + pub(crate) fn new(job: Job, outputs: Vec<Redirection>, inputs: Vec<Input>) -> Self { + PipeItem { + job, + outputs, + inputs, } } pub(crate) fn expand<E: Expander>(&mut self, expanders: &E) { - for job in &mut self.jobs { - job.expand(expanders); - } + self.job.expand(expanders); - let stdin = match self.stdin { - Some(Input::File(ref s)) => { - Some(Input::File(expand_string(s, expanders, false).join(" "))) - } - Some(Input::HereString(ref s)) => { - Some(Input::HereString(expand_string(s, expanders, true).join(" "))) - } - None => None, - }; + for input in self.inputs.iter_mut() { + *input = match input { + &mut Input::File(ref s) => + Input::File(expand_string(s, expanders, false).join(" ")), + &mut Input::HereString(ref s) => + Input::HereString(expand_string(s, expanders, true).join(" ")), + }; + } - self.stdin = stdin; + for output in self.outputs.iter_mut() { + output.file = expand_string(output.file.as_str(), expanders, false).join(" "); + } + } +} - if let Some(stdout) = self.stdout.iter_mut().next() { - stdout.file = expand_string(stdout.file.as_str(), expanders, false).join(" "); +impl Pipeline { + pub(crate) fn new() -> Self { + Pipeline { + items: Vec::new(), } } + pub(crate) fn expand<E: Expander>(&mut self, expanders: &E) { + self.items.iter_mut().for_each(|i| i.expand(expanders)); + } + pub(crate) fn requires_piping(&self) -> bool { - self.jobs.len() > 1 || self.stdin != None || self.stdout != None - || self.jobs.last().unwrap().kind == JobKind::Background + self.items.len() > 1 || self.items.iter().any(|it| it.outputs.len() > 0) + || self.items.iter().any(|it| it.inputs.len() > 0) + || self.items.last().unwrap().job.kind == JobKind::Background } } impl fmt::Display for Pipeline { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut tokens: Vec<String> = Vec::with_capacity(self.jobs.len()); - for job in &self.jobs { - tokens.extend(job.args.clone().into_iter()); - match job.kind { + let mut tokens: Vec<String> = Vec::with_capacity(self.items.len()); + for item in &self.items { + let job = &item.job; + let kind = job.kind; + let inputs = &item.inputs; + let outputs = &item.outputs; + tokens.extend(item.job.args.clone().into_iter()); + for input in inputs { + match input { + &Input::File(ref file) => { + tokens.push("<".into()); + tokens.push(file.clone()); + }, + &Input::HereString(ref string) => { + tokens.push("<<<".into()); + tokens.push(string.clone()); + }, + } + } + for output in outputs { + match output.from { + RedirectFrom::Stdout => { + tokens.push((if output.append { ">>" } else { ">" }).into()); + } + RedirectFrom::Stderr => { + tokens.push((if output.append { "^>>" } else { "^>" }).into()); + } + RedirectFrom::Both => { + tokens.push((if output.append { "&>>" } else { "&>" }).into()); + } + } + tokens.push(output.file.clone()); + } + match kind { JobKind::Last => (), JobKind::And => tokens.push("&&".into()), JobKind::Or => tokens.push("||".into()), @@ -89,31 +133,6 @@ impl fmt::Display for Pipeline { JobKind::Pipe(RedirectFrom::Both) => tokens.push("&|".into()), } } - match self.stdin { - None => (), - Some(Input::File(ref file)) => { - tokens.push("<".into()); - tokens.push(file.clone()); - } - Some(Input::HereString(ref string)) => { - tokens.push("<<<".into()); - tokens.push(string.clone()); - } - } - if let Some(ref outfile) = self.stdout { - match outfile.from { - RedirectFrom::Stdout => { - tokens.push((if outfile.append { ">>" } else { ">" }).into()); - } - RedirectFrom::Stderr => { - tokens.push((if outfile.append { "^>>" } else { "^>" }).into()); - } - RedirectFrom::Both => { - tokens.push((if outfile.append { "&>>" } else { "&>" }).into()); - } - } - tokens.push(outfile.file.clone()); - } write!(f, "{}", tokens.join(" ")) } diff --git a/src/parser/statement/parse.rs b/src/parser/statement/parse.rs index 297a420fd273364512a66fc26cfa22e1988fe042..15ae09f07e355c417f4e6fa50f42f5d9f8f2e87a 100644 --- a/src/parser/statement/parse.rs +++ b/src/parser/statement/parse.rs @@ -171,6 +171,7 @@ pub(crate) fn parse(code: &str) -> Statement { #[cfg(test)] mod tests { use super::*; + use self::pipelines::PipeItem; use parser::assignments::{KeyBuf, Primitive}; use shell::{Job, JobKind}; use shell::flow_control::Statement; @@ -180,9 +181,9 @@ mod tests { // Default case where spaced normally let parsed_if = parse("if test 1 -eq 2"); let correct_parse = Statement::If { - expression: Pipeline::new( - vec![ - Job::new( + expression: Pipeline { + items: vec![PipeItem { + job: Job::new( vec![ "test".to_owned(), "1".to_owned(), @@ -192,10 +193,10 @@ mod tests { .collect(), JobKind::Last, ), - ], - None, - None, - ), + outputs: Vec::new(), + inputs: Vec::new(), + }], + }, success: vec![], else_if: vec![], failure: vec![], diff --git a/src/shell/job.rs b/src/shell/job.rs index 46c6c82c7ae0f24eba98cd50aa9806f9e8c3f039..0b88fcef7dec0266f78d5cb6c059a7fddcb47869 100644 --- a/src/shell/job.rs +++ b/src/shell/job.rs @@ -1,5 +1,4 @@ use std::fs::File; -use std::os::unix::io::{FromRawFd, IntoRawFd}; use std::process::{Command, Stdio}; // use glob::glob; @@ -83,19 +82,90 @@ pub(crate) enum RefinedJob { /// A file corresponding to the standard error for this builtin stderr: Option<File>, }, + /// Represents redirection into stdin from more than one source + Cat { + sources: Vec<File>, + stdin: Option<File>, + stdout: Option<File>, + }, + Tee { + /// 0 for stdout, 1 for stderr + items: (Option<TeeItem>, Option<TeeItem>), + stdin: Option<File>, + stdout: Option<File>, + stderr: Option<File>, + }, +} + +pub struct TeeItem { + /// Where to read from for this tee. Generally only necessary if we need to tee both + /// stdout and stderr. + pub source: Option<File>, + pub sinks: Vec<File>, +} + +impl TeeItem { + /// Writes out to all destinations of a Tee. Takes an extra `RedirectFrom` argument in order to + /// handle piping. `RedirectFrom` paradoxically indicates where we are piping **to**. It should + /// never be `RedirectFrom`::Both` + pub(crate) fn write_to_all(&mut self, extra: Option<RedirectFrom>) -> ::std::io::Result<()> { + use ::std::io::{self, Write, Read}; + use ::std::os::unix::io::*; + fn write_out<R>(source: &mut R, sinks: &mut [File]) + -> io::Result<()> + where + R: Read + { + let mut buf = [0; 4096]; + loop { + // TODO: Figure out how to not block on this read + let len = source.read(&mut buf)?; + if len == 0 { return Ok(()); } + for file in sinks.iter_mut() { + let mut total = 0; + loop { + let wrote = file.write(&buf[total..len])?; + total += wrote; + if total == len { break; } + } + } + } + } + let stdout = io::stdout(); + let stderr = io::stderr(); + match extra { + None => {}, + Some(RedirectFrom::Stdout) => unsafe { + self.sinks.push(File::from_raw_fd(stdout.as_raw_fd())) + }, + Some(RedirectFrom::Stderr) => unsafe { + self.sinks.push(File::from_raw_fd(stderr.as_raw_fd())) + }, + Some(RedirectFrom::Both) => panic!("logic error! extra should never be RedirectFrom::Both"), + }; + if let Some(ref mut file) = self.source { + write_out(file, &mut self.sinks) + } else { + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + write_out(&mut stdin, &mut self.sinks) + } + } } macro_rules! set_field { ($self:expr, $field:ident, $arg:expr) => { match *$self { RefinedJob::External(ref mut command) => { - unsafe { - command.$field(Stdio::from_raw_fd($arg.into_raw_fd())); - } + command.$field(Stdio::from($arg)); } - RefinedJob::Builtin { ref mut $field, .. } | RefinedJob::Function { ref mut $field, .. } => { + RefinedJob::Builtin { ref mut $field, .. } | + RefinedJob::Function { ref mut $field, .. } | + RefinedJob::Tee { ref mut $field, .. } => { *$field = Some($arg); } + // Do nothing for Cat + _ => {} } } } @@ -121,12 +191,37 @@ impl RefinedJob { } } + pub(crate) fn cat(sources: Vec<File>) -> Self { + RefinedJob::Cat { + sources, + stdin: None, + stdout: None, + } + } + + pub(crate) fn tee(tee_out: Option<TeeItem>, tee_err: Option<TeeItem>) -> Self { + RefinedJob::Tee { + items: (tee_out, tee_err), + stdin: None, + stdout: None, + stderr: None, + } + } + pub(crate) fn stdin(&mut self, file: File) { - set_field!(self, stdin, file); + if let &mut RefinedJob::Cat { ref mut stdin, .. } = self { + *stdin = Some(file); + } else { + set_field!(self, stdin, file); + } } pub(crate) fn stdout(&mut self, file: File) { - set_field!(self, stdout, file); + if let &mut RefinedJob::Cat { ref mut stdout, .. } = self { + *stdout = Some(file); + } else { + set_field!(self, stdout, file); + } } pub(crate) fn stderr(&mut self, file: File) { @@ -143,6 +238,9 @@ impl RefinedJob { RefinedJob::Builtin { ref name, .. } | RefinedJob::Function { ref name, .. } => { name.to_string() } + // TODO: Print for real + RefinedJob::Cat { .. } => "multi-input".into(), + RefinedJob::Tee { .. } => "multi-output".into(), } } @@ -167,6 +265,10 @@ impl RefinedJob { RefinedJob::Builtin { ref args, .. } | RefinedJob::Function { ref args, .. } => { format!("{}", args.join(" ")) } + // TODO: Figure out real printing + RefinedJob::Cat { .. } | RefinedJob::Tee { .. } => { + "".into() + } } } } diff --git a/src/shell/mod.rs b/src/shell/mod.rs index cc58c492dce66784c506d03f0719d9625e462e39..39d48757454d347b7cd66b9ba528f9a3fbbcd70c 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -210,23 +210,23 @@ impl<'a> Shell<'a> { let builtins = self.builtins; // Expand any aliases found - for job_no in 0..pipeline.jobs.len() { + for job_no in 0..pipeline.items.len() { if let Some(alias) = { - let key: &str = pipeline.jobs[job_no].command.as_ref(); + let key: &str = pipeline.items[job_no].job.command.as_ref(); self.variables.aliases.get(key) } { let new_args = ArgumentSplitter::new(alias) .map(String::from) - .chain(pipeline.jobs[job_no].args.drain().skip(1)) + .chain(pipeline.items[job_no].job.args.drain().skip(1)) .collect::<SmallVec<[String; 4]>>(); - pipeline.jobs[job_no].command = new_args[0].clone().into(); - pipeline.jobs[job_no].args = new_args; + pipeline.items[job_no].job.command = new_args[0].clone().into(); + pipeline.items[job_no].job.args = new_args; } } // Branch if -> input == shell command i.e. echo let exit_status = if let Some(command) = { - let key: &str = pipeline.jobs[0].command.as_ref(); + let key: &str = pipeline.items[0].job.command.as_ref(); builtins.get(key) } { pipeline.expand(self); @@ -235,7 +235,7 @@ impl<'a> Shell<'a> { if self.flags & PRINT_COMMS != 0 { eprintln!("> {}", pipeline.to_string()); } - let borrowed = &pipeline.jobs[0].args; + let borrowed = &pipeline.items[0].job.args; let small: SmallVec<[&str; 4]> = borrowed.iter().map(|x| x as &str).collect(); if self.flags & NO_EXEC != 0 { Some(SUCCESS) @@ -246,9 +246,9 @@ impl<'a> Shell<'a> { Some(self.execute_pipeline(pipeline)) } // Branch else if -> input == shell function and set the exit_status - } else if let Some(function) = self.functions.get(&pipeline.jobs[0].command).cloned() { + } else if let Some(function) = self.functions.get(&pipeline.items[0].job.command).cloned() { if !pipeline.requires_piping() { - let args: &[String] = pipeline.jobs[0].args.deref(); + let args: &[String] = pipeline.items[0].job.args.deref(); let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect(); match function.execute(self, &args) { Ok(()) => None, diff --git a/src/shell/pipe_exec/mod.rs b/src/shell/pipe_exec/mod.rs index a7a591f3c4ff8620cf8932c7982da83f018bccaa..53d2d012e9642fb5982007e2ccc0230251aa46e0 100644 --- a/src/shell/pipe_exec/mod.rs +++ b/src/shell/pipe_exec/mod.rs @@ -13,10 +13,10 @@ use self::job_control::JobControl; use super::{JobKind, Shell}; use super::flags::*; use super::flow_control::FunctionError; -use super::job::RefinedJob; +use super::job::{RefinedJob, TeeItem}; use super::signals::{self, SignalHandler}; use super::status::*; -use parser::pipelines::{Input, Pipeline, RedirectFrom, Redirection}; +use parser::pipelines::{Input, PipeItem, Pipeline, RedirectFrom, Redirection}; use std::fs::{File, OpenOptions}; use std::io::{self, Error, Write}; use std::iter; @@ -26,6 +26,8 @@ use std::path::Path; use std::process::{exit, Command}; use sys; +type RefinedItem = (RefinedJob, JobKind, Vec<Redirection>, Vec<Input>); + /// Use dup2 to replace `old` with `new` using `old`s file descriptor ID fn redir(old: RawFd, new: RawFd) { if let Err(e) = sys::dup2(old, new) { @@ -53,7 +55,7 @@ pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<RawFd, Error> { /// 2. The value stored within `Some` will be that background job's command name. /// 3. If `set -x` was set, print the command. fn gen_background_string(pipeline: &Pipeline, print_comm: bool) -> Option<String> { - if pipeline.jobs[pipeline.jobs.len() - 1].kind == JobKind::Background { + if pipeline.items[pipeline.items.len() - 1].job.kind == JobKind::Background { let command = pipeline.to_string(); if print_comm { eprintln!("> {}", command); @@ -78,84 +80,232 @@ fn is_implicit_cd(argument: &str) -> bool { && Path::new(argument).is_dir() } -/// This function is to be executed when a stdin value is supplied to a pipeline job. -/// -/// Using that value, the stdin of the first command will be mapped to either a `File`, -/// or `HereString`, which may be either a herestring or heredoc. Returns `true` if -/// the input error occurred. -fn redirect_input(mut input: Input, piped_commands: &mut Vec<(RefinedJob, JobKind)>) -> bool { - match input { - Input::File(ref filename) => if let Some(command) = piped_commands.first_mut() { - match File::open(filename) { - Ok(file) => command.0.stdin(file), - Err(e) => { - eprintln!("ion: failed to redirect '{}' into stdin: {}", filename, e); - return true; +/// Insert the multiple redirects as pipelines if necessary. Handle both input and output +/// redirection if necessary. +fn do_redirection(piped_commands: Vec<RefinedItem>) + -> Option<Vec<(RefinedJob, JobKind)>> { + macro_rules! get_infile { + ($input:expr) => { + match $input { + Input::File(ref filename) => match File::open(filename) { + Ok(file) => Some(file), + Err(e) => { + eprintln!("ion: failed to redirect '{}' to stdin: {}", filename, e); + None + } + }, + Input::HereString(ref mut string) => { + if !string.ends_with('\n') { + string.push('\n'); + } + match unsafe { stdin_of(&string) } { + Ok(stdio) => Some(unsafe { File::from_raw_fd(stdio) }), + Err(e) => { + eprintln!("ion: failed to redirect herestring '{}' to stdin: {}", + string, e); + None + } + } } } - }, - Input::HereString(ref mut string) => if let Some(command) = piped_commands.first_mut() { - if !string.ends_with('\n') { - string.push('\n'); - } - match unsafe { stdin_of(&string) } { - Ok(stdio) => { - command.0.stdin(unsafe { File::from_raw_fd(stdio) }); + } + } + + let need_tee = |outs: &[_], kind| { + let (mut stdout_count, mut stderr_count) = (0, 0); + match kind { + JobKind::Pipe(RedirectFrom::Both) => { + stdout_count += 1; + stderr_count += 1; + }, + JobKind::Pipe(RedirectFrom::Stdout) => stdout_count += 1, + JobKind::Pipe(RedirectFrom::Stderr) => stderr_count += 1, + _ => {} + } + for out in outs { + let &Redirection { from, .. } = out; + match from { + RedirectFrom::Both => { + stdout_count += 1; + stderr_count += 1; } - Err(e) => { - eprintln!("ion: failed to redirect herestring '{}' into stdin: {}", string, e); - return true; + RedirectFrom::Stdout => stdout_count += 1, + RedirectFrom::Stderr => stderr_count += 1, + } + if stdout_count >= 2 && stderr_count >= 2 { + return (true, true); + } + } + (stdout_count >= 2, stderr_count >= 2) + }; + + macro_rules! set_no_tee { + ($outputs:ident, $job:ident) => { + // XXX: Possibly add an assertion here for correctness + for output in $outputs { + match if output.append { + OpenOptions::new().create(true).write(true).append(true).open(&output.file) + } else { + File::create(&output.file) + } { + Ok(f) => match output.from { + RedirectFrom::Stderr => $job.stderr(f), + RedirectFrom::Stdout => $job.stdout(f), + RedirectFrom::Both => match f.try_clone() { + Ok(f_copy) => { + $job.stdout(f); + $job.stderr(f_copy); + }, + Err(e) => { + eprintln!( + "ion: failed to redirect both stdout and stderr to file '{:?}': {}", + f, + e); + return None; + } + } + }, + Err(e) => { + eprintln!("ion: failed to redirect output into {}: {}", output.file, e); + return None; + } } } - }, + } } - false -} -/// This function is to be executed when a stdout/stderr value is supplied to a pipeline job. -/// -/// Using that value, the stdout and/or stderr of the last command will be redirected accordingly -/// to the designated output. Returns `true` if the outputs couldn't be redirected. -fn redirect_output(stdout: Redirection, piped_commands: &mut Vec<(RefinedJob, JobKind)>) -> bool { - if let Some(command) = piped_commands.last_mut() { - let file = if stdout.append { - OpenOptions::new().create(true).write(true).append(true).open(&stdout.file) - } else { - File::create(&stdout.file) - }; - match file { - Ok(f) => match stdout.from { - RedirectFrom::Both => match f.try_clone() { - Ok(f_copy) => { - command.0.stdout(f); - command.0.stderr(f_copy); - } + macro_rules! set_one_tee { + ($new:ident, $outputs:ident, $job:ident, $kind:ident, $teed:ident, $other:ident) => {{ + let mut tee = TeeItem { sinks: Vec::new(), source: None }; + for output in $outputs { + match if output.append { + OpenOptions::new().create(true).write(true).append(true).open(&output.file) + } else { + File::create(&output.file) + } { + Ok(f) => match output.from { + RedirectFrom::$teed => tee.sinks.push(f), + RedirectFrom::$other => if RedirectFrom::Stdout == RedirectFrom::$teed { + $job.stderr(f); + } else { + $job.stdout(f); + }, + RedirectFrom::Both => match f.try_clone() { + Ok(f_copy) => { + if RedirectFrom::Stdout == RedirectFrom::$teed { + $job.stderr(f); + } else { + $job.stdout(f); + } + tee.sinks.push(f_copy); + }, + Err(e) => { + eprintln!( + "ion: failed to redirect both stdout and stderr to file '{:?}': {}", + f, + e); + return None; + } + } + }, Err(e) => { - eprintln!( - "ion: failed to redirect both stderr and stdout into file '{:?}': {}", - f, - e - ); - return true; + eprintln!("ion: failed to redirect output into {}: {}", output.file, e); + return None; } - }, - RedirectFrom::Stderr => command.0.stderr(f), - RedirectFrom::Stdout => command.0.stdout(f), + } + } + $new.push(($job, JobKind::Pipe(RedirectFrom::$teed))); + let items = if RedirectFrom::Stdout == RedirectFrom::$teed { + (Some(tee), None) + } else { + (None, Some(tee)) + }; + let tee = RefinedJob::tee(items.0, items.1); + $new.push((tee, $kind)); + }} + } + + + // Real logic begins here + let mut new_commands = Vec::new(); + let mut prev_kind = JobKind::And; + for (mut job, kind, outputs, mut inputs) in piped_commands { + match (inputs.len(), prev_kind) { + (0, _) => {}, + (1, JobKind::Pipe(_)) => { + let sources = vec![get_infile!(inputs[0])?]; + new_commands.push((RefinedJob::cat(sources), + JobKind::Pipe(RedirectFrom::Stdout))); }, - Err(err) => { - let stderr = io::stderr(); - let mut stderr = stderr.lock(); - let _ = writeln!( - stderr, - "ion: failed to redirect stdout into {}: {}", - stdout.file, - err - ); - return true; + (1, _) => job.stdin(get_infile!(inputs[0])?), + _ => { + let mut sources = Vec::new(); + for mut input in inputs { + sources.push(if let Some(f) = get_infile!(input) { + f + } else { + return None; + }); + } + new_commands.push((RefinedJob::cat(sources), + JobKind::Pipe(RedirectFrom::Stdout))); + }, + } + prev_kind = kind; + if outputs.is_empty() { + new_commands.push((job, kind)); + continue; + } + match need_tee(&outputs, kind) { + // No tees + (false, false) => { + set_no_tee!(outputs, job); + new_commands.push((job, kind)); + } + // tee stderr + (false, true) => set_one_tee!(new_commands, outputs, job, kind, Stderr, Stdout), + // tee stdout + (true, false) => set_one_tee!(new_commands, outputs, job, kind, Stdout, Stderr), + // tee both + (true, true) => { + let mut tee_out = TeeItem { sinks: Vec::new(), source: None }; + let mut tee_err = TeeItem { sinks: Vec::new(), source: None }; + for output in outputs { + match if output.append { + OpenOptions::new().create(true).write(true).append(true).open(&output.file) + } else { + File::create(&output.file) + } { + Ok(f) => match output.from { + RedirectFrom::Stdout => tee_out.sinks.push(f), + RedirectFrom::Stderr => tee_err.sinks.push(f), + RedirectFrom::Both => match f.try_clone() { + Ok(f_copy) => { + tee_out.sinks.push(f); + tee_err.sinks.push(f_copy); + }, + Err(e) => { + eprintln!( + "ion: failed to redirect both stdout and stderr to file '{:?}': {}", + f, + e); + return None; + } + } + }, + Err(e) => { + eprintln!("ion: failed to redirect output into {}: {}", output.file, e); + return None; + } + } + } + let tee = RefinedJob::tee(Some(tee_out), Some(tee_err)); + new_commands.push((job, JobKind::Pipe(RedirectFrom::Stdout))); + new_commands.push((tee, kind)); } } } - false + Some(new_commands) } pub(crate) trait PipelineExecution { @@ -183,7 +333,7 @@ pub(crate) trait PipelineExecution { /// Each generated command will either be a builtin or external command, and will be /// associated will be marked as an `&&`, `||`, `|`, or final job. fn generate_commands(&self, pipeline: &mut Pipeline) - -> Result<Vec<(RefinedJob, JobKind)>, i32>; + -> Result<Vec<RefinedItem>, i32>; /// Waits for all of the children within a pipe to finish exuecting, returning the /// exit status of the last process in the queue. @@ -220,6 +370,22 @@ pub(crate) trait PipelineExecution { stderr: &Option<File>, stdin: &Option<File>, ) -> i32; + + /// For cat jobs + fn exec_multi_in(&mut self, + sources: &mut [File], + stdout: &Option<File>, + stdin: &mut Option<File>, + ) -> i32; + + /// For tee jobs + fn exec_multi_out(&mut self, + items: &mut (Option<TeeItem>, Option<TeeItem>), + stdout: &Option<File>, + stderr: &Option<File>, + stdin: &Option<File>, + kind: JobKind + ) -> i32; } impl<'a> PipelineExecution for Shell<'a> { @@ -231,7 +397,7 @@ impl<'a> PipelineExecution for Shell<'a> { let possible_background_name = gen_background_string(&pipeline, self.flags & PRINT_COMMS != 0); // Generates commands for execution, differentiating between external and builtin commands. - let mut piped_commands = match self.generate_commands(pipeline) { + let piped_commands = match self.generate_commands(pipeline) { Ok(commands) => commands, Err(error) => return error, }; @@ -241,18 +407,12 @@ impl<'a> PipelineExecution for Shell<'a> { return SUCCESS; } - // Redirect the inputs if a custom redirect value was given. - if let Some(stdin) = pipeline.stdin.take() { - if redirect_input(stdin, &mut piped_commands) { - return COULD_NOT_EXEC; - } - } - // Redirect the outputs if a custom redirect value was given. - if let Some(stdout) = pipeline.stdout.take() { - if redirect_output(stdout, &mut piped_commands) { - return COULD_NOT_EXEC; - } - } + let piped_commands = if let Some(c) = do_redirection(piped_commands) { + c + } else { + return COULD_NOT_EXEC; + }; + // If the given pipeline is a background task, fork the shell. if let Some(command_name) = possible_background_name { fork_pipe(self, piped_commands, command_name) @@ -273,9 +433,10 @@ impl<'a> PipelineExecution for Shell<'a> { fn generate_commands( &self, pipeline: &mut Pipeline, - ) -> Result<Vec<(RefinedJob, JobKind)>, i32> { + ) -> Result<Vec<RefinedItem>, i32> { let mut results = Vec::new(); - for mut job in pipeline.jobs.drain(..) { + for item in pipeline.items.drain(..) { + let PipeItem { mut job, outputs, inputs } = item; let refined = { if is_implicit_cd(&job.args[0]) { RefinedJob::builtin( @@ -294,7 +455,7 @@ impl<'a> PipelineExecution for Shell<'a> { RefinedJob::External(command) } }; - results.push((refined, job.kind)); + results.push((refined, job.kind, outputs, inputs)); } Ok(results) } @@ -396,6 +557,7 @@ impl<'a> PipelineExecution for Shell<'a> { eprintln!("ion: failed to `dup` STDOUT, STDIN, or STDERR: not running '{}'", long); COULD_NOT_EXEC } + _ => panic!("exec job should not be able to be called on Cat or Tee jobs"), } } @@ -457,6 +619,95 @@ impl<'a> PipelineExecution for Shell<'a> { } } } + + fn exec_multi_in( + &mut self, + sources: &mut [File], + stdout: &Option<File>, + stdin: &mut Option<File>, + ) -> i32 { + if let Some(ref file) = *stdin { + redir(file.as_raw_fd(), sys::STDIN_FILENO) + } + if let Some(ref file) = *stdout { + redir(file.as_raw_fd(), sys::STDOUT_FILENO) + } + + fn read_and_write<R: io::Read>(src: &mut R, stdout: &mut io::StdoutLock) + -> io::Result<()> { + let mut buf = [0; 4096]; + loop { + let len = src.read(&mut buf)?; + if len == 0 { return Ok(()) }; + let mut total = 0; + loop { + let wrote = stdout.write(&buf[total..len])?; + total += wrote; + if total == len { break; } + } + } + }; + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + for file in stdin.iter_mut().chain(sources) { + match read_and_write(file, &mut stdout) { + Ok(_) => {} + Err(e) => { + eprintln!( + "ion: error in multiple input redirect process: {:?}", + e + ); + return FAILURE; + } + } + } + SUCCESS + } + + fn exec_multi_out(&mut self, + items: &mut (Option<TeeItem>, Option<TeeItem>), + stdout: &Option<File>, + stderr: &Option<File>, + stdin: &Option<File>, + kind: JobKind + ) -> i32 { + if let Some(ref file) = *stdin { + redir(file.as_raw_fd(), sys::STDIN_FILENO); + } + if let Some(ref file) = *stdout { + redir(file.as_raw_fd(), sys::STDOUT_FILENO); + } + if let Some(ref file) = *stderr { + redir(file.as_raw_fd(), sys::STDERR_FILENO); + } + let res = match items { + &mut (None, None) => panic!("There must be at least one TeeItem, this is a bug"), + &mut (Some(ref mut tee_out), None) => match kind { + JobKind::Pipe(RedirectFrom::Stderr) => tee_out.write_to_all(None), + JobKind::Pipe(_) => tee_out.write_to_all(Some(RedirectFrom::Stdout)), + _ => tee_out.write_to_all(None), + } + &mut (None, Some(ref mut tee_err)) => match kind { + JobKind::Pipe(RedirectFrom::Stdout) => tee_err.write_to_all(None), + JobKind::Pipe(_) => tee_err.write_to_all(Some(RedirectFrom::Stderr)), + _ => tee_err.write_to_all(None), + } + &mut (Some(ref mut tee_out), Some(ref mut tee_err)) => { + // TODO Make it work with pipes + if let Err(e) = tee_out.write_to_all(None) { + Err(e) + } else { + tee_err.write_to_all(None) + } + } + }; + if let Err(e) = res { + eprintln!("ion: error in multiple output redirection process: {:?}", e); + FAILURE + } else { + SUCCESS + } + } } /// This function will panic if called with an empty slice @@ -476,6 +727,10 @@ pub(crate) fn pipe( let mut previous_status = SUCCESS; let mut previous_kind = JobKind::And; let mut commands = commands.into_iter(); + // A vector to hold possible external command stdout/stderr pipes. + // If it is Some, then we close the various file descriptors after + // spawning a child job. + let mut ext_stdio: Option<Vec<RawFd>> = None; loop { if let Some((mut parent, mut kind)) = commands.next() { // When an `&&` or `||` operator is utilized, execute commands based on the previous @@ -569,6 +824,8 @@ pub(crate) fn pipe( exit(ret) }, Ok(pid) => { + close(stdout); + close(stderr); if pgid == 0 { pgid = pid; if foreground && !shell.is_library { @@ -612,6 +869,8 @@ pub(crate) fn pipe( exit(ret) }, Ok(pid) => { + close(stdout); + close(stderr); if pgid == 0 { pgid = pid; if foreground && !shell.is_library { @@ -628,37 +887,146 @@ pub(crate) fn pipe( } } } + RefinedJob::Cat { ref mut sources, + ref stdout, + ref mut stdin } => { + match unsafe { sys::fork() } { + Ok(0) => { + let _ = sys::reset_signal(sys::SIGINT); + let _ = sys::reset_signal(sys::SIGHUP); + let _ = sys::reset_signal(sys::SIGTERM); + create_process_group(pgid); + let ret = shell.exec_multi_in( + sources, + stdout, + stdin, + ); + close(stdout); + close(stdin); + exit(ret); + } + Ok(pid) => { + close(stdout); + if pgid == 0 { + pgid = pid; + if foreground && !shell.is_library { + let _ = sys::tcsetpgrp(0, pgid); + } + } + shell.foreground.push(pid); + children.push(pid); + } + Err(e) => eprintln!("ion: failed to fork {}: {}", short, e), + } + }, + RefinedJob::Tee { ref mut items, + ref stdout, + ref stderr, + ref stdin } => { + match unsafe { sys::fork() } { + Ok(0) => { + let _ = sys::reset_signal(sys::SIGINT); + let _ = sys::reset_signal(sys::SIGHUP); + let _ = sys::reset_signal(sys::SIGTERM); + create_process_group(pgid); + let ret = shell.exec_multi_out( + items, + stdout, + stderr, + stdin, + kind, + ); + close(stdout); + close(stderr); + close(stdin); + exit(ret); + }, + Ok(pid) => { + close(stdout); + close(stderr); + if pgid == 0 { + pgid = pid; + if foreground && !shell.is_library { + let _ = sys::tcsetpgrp(0, pgid); + } + } + shell.foreground.push(pid); + children.push(pid); + } + Err(e) => eprintln!("ion: failed to fork {}: {}", short, e), + } + } } }; } // Append other jobs until all piped jobs are running while let Some((mut child, ckind)) = commands.next() { - match sys::pipe2(sys::O_CLOEXEC) { - Err(e) => { - eprintln!("ion: failed to create pipe: {:?}", e); + // If parent is a RefindJob::External, then we need to keep track of the + // output pipes, so we can properly close them after the job has been + // spawned. + let is_external = if let RefinedJob::External(..) = parent { + true + } else { + false + }; + + // If we need to tee both stdout and stderr, we directly connect pipes to + // the relevant sources in both of them. + if let RefinedJob::Tee { + items: (Some(ref mut tee_out), Some(ref mut tee_err)), + .. + } = child { + match sys::pipe2(sys::O_CLOEXEC) { + Err(e) => eprintln!("ion: failed to create pipe: {:?}", e), + Ok((out_reader, out_writer)) => { + (*tee_out).source = Some(unsafe { File::from_raw_fd(out_reader) }); + parent.stdout(unsafe { File::from_raw_fd(out_writer) }); + if is_external { + ext_stdio.get_or_insert(vec![]).push(out_writer); + } + } } - Ok((reader, writer)) => { - child.stdin(unsafe { File::from_raw_fd(reader) }); - match mode { - RedirectFrom::Stderr => { - parent.stderr(unsafe { File::from_raw_fd(writer) }); + match sys::pipe2(sys::O_CLOEXEC) { + Err(e) => eprintln!("ion: failed to create pipe: {:?}", e), + Ok((err_reader, err_writer)) => { + (*tee_err).source = Some(unsafe { File::from_raw_fd(err_reader) }); + parent.stderr(unsafe { File::from_raw_fd(err_writer) }); + if is_external { + ext_stdio.get_or_insert(vec![]).push(err_writer); } - RedirectFrom::Stdout => { - parent.stdout(unsafe { File::from_raw_fd(writer) }); + } + } + } else { + match sys::pipe2(sys::O_CLOEXEC) { + Err(e) => { + eprintln!("ion: failed to create pipe: {:?}", e); + } + Ok((reader, writer)) => { + if is_external { + ext_stdio.get_or_insert(vec![]).push(writer); } - RedirectFrom::Both => { - let temp = unsafe { File::from_raw_fd(writer) }; - match temp.try_clone() { - Err(e) => { - eprintln!( - "ion: failed to redirect stdout and stderr: {}", - e - ); - } - Ok(duped) => { - parent.stderr(temp); - parent.stdout(duped); + child.stdin(unsafe { File::from_raw_fd(reader) }); + match mode { + RedirectFrom::Stderr => { + parent.stderr(unsafe { File::from_raw_fd(writer) }); + } + RedirectFrom::Stdout => { + parent.stdout(unsafe { File::from_raw_fd(writer) }); + } + RedirectFrom::Both => { + let temp = unsafe { File::from_raw_fd(writer) }; + match temp.try_clone() { + Err(e) => { + eprintln!( + "ion: failed to redirect stdout and stderr: {}", + e + ); + } + Ok(duped) => { + parent.stderr(temp); + parent.stdout(duped); + } } } } @@ -667,6 +1035,13 @@ pub(crate) fn pipe( } spawn_proc!(parent); remember.push(parent); + if let Some(fds) = ext_stdio.take() { + for fd in fds { + if let Err(e) = sys::close(fd) { + eprintln!("ion: failed to close file '{:?}': {}", fd, e); + } + } + } if let JobKind::Pipe(m) = ckind { parent = child; mode = m;