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;