diff --git a/examples/builtin_piping.ion b/examples/builtin_piping.ion
new file mode 100644
index 0000000000000000000000000000000000000000..47402a93a78c896b6bef0e80e8cce9f33dace157
--- /dev/null
+++ b/examples/builtin_piping.ion
@@ -0,0 +1,6 @@
+matches Foo '([A-Z])\w+' && echo true
+matches foo '([A-Z])\w+' || echo false
+
+fn foobar x; end
+
+fn | tr '[a-z]' '[A-Z]'
diff --git a/examples/builtin_piping.out b/examples/builtin_piping.out
new file mode 100644
index 0000000000000000000000000000000000000000..cf77dde3309b35ced0f45d4df83c57d33ceb756a
--- /dev/null
+++ b/examples/builtin_piping.out
@@ -0,0 +1,4 @@
+true
+false
+# FUNCTIONS
+    FOOBAR
diff --git a/src/shell/fork.rs b/src/shell/fork.rs
index 93ef914bee1e3c7ebb6f8c152aae61bad797d434..36bacc362cdeebc7ed6986b585130c88e5cc69b8 100644
--- a/src/shell/fork.rs
+++ b/src/shell/fork.rs
@@ -5,8 +5,8 @@ pub fn create_process_group(pgid: u32) {
     let _ = sys::setpgid(0, pgid);
 }
 
-use std::process::{Command, exit};
-use super::job::JobKind;
+use std::process::exit;
+use super::job::{RefinedJob, JobKind};
 use super::job_control::{JobControl, ProcessState};
 use super::Shell;
 use super::signals;
@@ -17,7 +17,7 @@ use super::pipe::pipe;
 /// the given commands in the child fork.
 pub fn fork_pipe (
     shell: &mut Shell,
-    commands: Vec<(Command, JobKind)>,
+    commands: Vec<(RefinedJob, JobKind)>,
     command_name: String
 ) -> i32 {
     match unsafe { sys::fork() } {
diff --git a/src/shell/job.rs b/src/shell/job.rs
index 02d75ae2ec2fb9cb7b59c67110f8f166987b1ab9..985a230ecff4940d974becee0646f57f3eacf18b 100644
--- a/src/shell/job.rs
+++ b/src/shell/job.rs
@@ -1,9 +1,11 @@
-use std::process::Command;
+use std::process::{Command, Stdio};
+use std::os::unix::io::{RawFd, FromRawFd};
 
 //use glob::glob;
 use parser::{expand_string, ExpanderFunctions};
 use parser::peg::RedirectFrom;
 use smallstring::SmallString;
+use sys;
 use types::*;
 
 #[derive(Debug, PartialEq, Clone, Copy)]
@@ -19,48 +21,152 @@ pub struct Job {
 impl Job {
     pub fn new(args: Array, kind: JobKind) -> Self {
         let command = SmallString::from_str(&args[0]);
-        Job {
-            command: command,
-            args: args,
-            kind: kind,
-        }
+        Job { command, args, kind }
     }
 
     /// Takes the current job's arguments and expands them, one argument at a
     /// time, returning a new `Job` with the expanded arguments.
     pub fn expand(&mut self, expanders: &ExpanderFunctions) {
-        use smallvec::SmallVec;
-
-        let mut expanded = SmallVec::new();
+        let mut expanded = Array::new();
         expanded.grow(self.args.len());
-        {
-            for arg in self.args.drain().flat_map(|argument| expand_string(&argument, expanders, false)) {
+        expanded.extend(self.args.drain().flat_map(|arg| {
+            expand_string(&arg, expanders, false)
+        }));
+        self.args = expanded;
+    }
+
+}
+
+/// This represents a job that has been processed and expanded to be run
+/// as part of some pipeline
+pub enum RefinedJob {
+    /// An external program that is executed by this shell
+    External(Command),
+    /// A procedure embedded into Ion
+    Builtin {
+        /// Name of the procedure
+        name: Identifier,
+        /// Arguments to pass in to the procedure
+        args: Array,
+        /// A file corresponding to the standard input for this builtin
+        stdin: Option<RawFd>,
+        /// A file corresponding to the standard output for this builtin
+        stdout: Option<RawFd>,
+        /// A file corresponding to the standard error for this builtin
+        stderr: Option<RawFd>,
+    }
+}
 
-                expanded.push(arg);
+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));
+                }
+            }
+            RefinedJob::Builtin { ref mut $field,  .. } => {
+                *$field = Some($arg);
             }
         }
+    }
+}
 
-        self.args = expanded;
-        self.command = self.args.first().map_or("".into(), |c| c.clone().into());
+impl RefinedJob {
+
+    pub fn builtin(name: Identifier, args: Array) -> Self {
+        RefinedJob::Builtin {
+            name,
+            args,
+            stdin: None,
+            stdout: None,
+            stderr: None
+        }
     }
 
-    pub fn build_command_external(&mut self) -> Command {
-        let mut command = Command::new(&self.command);
-        for arg in self.args.drain().skip(1) {
-            command.arg(arg);
+    pub fn stdin(&mut self, fd: RawFd) {
+        set_field!(self, stdin, fd);
+    }
+
+    pub fn stdout(&mut self, fd: RawFd) {
+        set_field!(self, stdout, fd);
+    }
+
+    pub fn stderr(&mut self, fd: RawFd) {
+        set_field!(self, stderr, fd);
+    }
+
+    /// Returns a short description of this job: often just the command
+    /// or builtin name
+    pub fn short(&self) -> String {
+        match *self {
+            RefinedJob::External(ref cmd) => {
+                format!("{:?}", cmd).split('"').nth(1).unwrap_or("").to_string()
+            },
+            RefinedJob::Builtin { ref name, .. } => {
+                name.to_string()
+            }
         }
-        command
     }
 
-    pub fn build_command_builtin(&mut self) -> Command {
-        use std::env;
-        let process = env::current_exe().unwrap();
-        let mut command = Command::new(process);
-        command.arg("-c");
-        command.arg(&self.command);
-        for arg in self.args.drain().skip(1) {
-            command.arg(arg);
+    /// Returns a long description of this job: the commands and arguments
+    pub fn long(&self) -> String {
+        match *self {
+            RefinedJob::External(ref cmd) => {
+                let command = format!("{:?}", cmd);
+                let mut arg_iter = command.split_whitespace();
+                let command = arg_iter.next().unwrap();
+                let mut output = String::from(&command[1..command.len()-1]);
+                for argument in arg_iter {
+                    output.push(' ');
+                    if argument.len() > 2 {
+                        output.push_str(&argument[1..argument.len()-1]);
+                    } else {
+                        output.push_str(&argument);
+                    }
+                }
+                output
+            },
+            RefinedJob::Builtin { ref args, .. } => {
+                format!("{}", args.join(" "))
+            }
         }
-        command
     }
+
+}
+
+impl Drop for RefinedJob {
+
+    // This is needed in order to ensure that the parent instance of RefinedJob
+    // cleans up after its own `RawFd`s; otherwise these would never be properly
+    // closed, never sending EOF, causing any process reading from these
+    // `RawFd`s to halt indefinitely.
+    fn drop(&mut self) {
+        match *self {
+            RefinedJob::External(ref mut cmd) => {
+                drop(cmd);
+            },
+            RefinedJob::Builtin {
+                ref mut name,
+                ref mut args,
+                ref mut stdin,
+                ref mut stdout,
+                ref mut stderr,
+            } => {
+                fn close(fd: Option<RawFd>) {
+                    if let Some(fd) = fd {
+                        if let Err(e) = sys::close(fd) {
+                            eprintln!("ion: failed to close file '{}': {}", fd, e);
+                        }
+                    }
+                }
+                drop(name);
+                drop(args);
+                close(*stdin);
+                close(*stdout);
+                close(*stderr);
+            }
+        }
+    }
+
 }
diff --git a/src/shell/pipe.rs b/src/shell/pipe.rs
index 1d6dbf098e901fb6dd08b705bc04e9c9ece6c102..d9df241928b4e7453285757cffccf7fc006c4266 100644
--- a/src/shell/pipe.rs
+++ b/src/shell/pipe.rs
@@ -1,21 +1,30 @@
 use std::io::{self, Error, Write};
-use std::process::{Stdio, Command};
-use std::os::unix::io::{FromRawFd, IntoRawFd};
+use std::process::{Command, exit};
+use std::os::unix::io::{FromRawFd, IntoRawFd, RawFd};
 use std::os::unix::process::CommandExt;
 use std::fs::{File, OpenOptions};
 use super::flags::*;
 use super::fork::{fork_pipe, create_process_group};
 use super::job_control::JobControl;
 use super::{JobKind, Shell};
+use super::job::RefinedJob;
 use super::status::*;
 use super::signals::{self, SignalHandler};
 use parser::peg::{Pipeline, Input, RedirectFrom};
 use sys;
 
-/// Create an instance of Stdio from a byte slice that will echo the
-/// contents of the slice when read. This can be called with owned or
-/// borrowed strings
-pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<Stdio, Error> {
+/// Use dup2 to replace `old` with `new` using `old`s file descriptor ID
+pub fn redir(old: RawFd, new: RawFd) {
+    if let Err(e) = sys::dup2(old, new) {
+        eprintln!("ion: could not duplicate {} to {}: {}", old, new, e);
+    }
+}
+
+
+/// Create an OS pipe and write the contents of a byte slice to one end
+/// such that reading from this pipe will produce the byte slice. Return
+/// A file descriptor representing the read end of the pipe.
+pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<RawFd, Error> {
     let (reader, writer) = sys::pipe2(sys::O_CLOEXEC)?;
     let mut infile = File::from_raw_fd(writer);
     // Write the contents; make sure to use write_all so that we block until
@@ -24,36 +33,7 @@ pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<Stdio, Error> {
     infile.flush()?;
     // `infile` currently owns the writer end RawFd. If we just return the reader end
     // and let `infile` go out of scope, it will be closed, sending EOF to the reader!
-    Ok(Stdio::from_raw_fd(reader))
-}
-
-/// Set up pipes such that the relevant output of parent is sent to the stdin of child.
-/// The content that is sent depends on `mode`
-pub unsafe fn create_pipe (
-    parent: &mut Command,
-    child: &mut Command,
-    mode: RedirectFrom
-) -> Result<(), Error> {
-    let (reader, writer) = sys::pipe2(sys::O_CLOEXEC)?;
-    match mode {
-        RedirectFrom::Stdout => {
-            parent.stdout(Stdio::from_raw_fd(writer));
-        },
-        RedirectFrom::Stderr => {
-            parent.stderr(Stdio::from_raw_fd(writer));
-        },
-        RedirectFrom::Both => {
-            let temp_file = File::from_raw_fd(writer);
-            let clone = temp_file.try_clone()?;
-            // We want to make sure that the temp file we created no longer has ownership
-            // over the raw file descriptor otherwise it gets closed
-            temp_file.into_raw_fd();
-            parent.stdout(Stdio::from_raw_fd(writer));
-            parent.stderr(Stdio::from_raw_fd(clone.into_raw_fd()));
-        }
-    }
-    child.stdin(Stdio::from_raw_fd(reader));
-    Ok(())
+    Ok(reader)
 }
 
 /// This function serves three purposes:
@@ -81,25 +61,39 @@ impl<'a> PipelineExecution for Shell<'a> {
     fn execute_pipeline(&mut self, pipeline: &mut Pipeline) -> i32 {
         let background_string = check_if_background_job(&pipeline, self.flags & PRINT_COMMS != 0);
 
-        // Generate a list of commands from the given pipeline
-        let mut piped_commands: Vec<(Command, JobKind)> = pipeline.jobs
-            .drain(..).map(|mut job| {
-                if self.builtins.contains_key(&job.command.as_ref()) {
-                    (job.build_command_builtin(), job.kind)
-                } else {
-                    (job.build_command_external(), job.kind)
-                }
-            }).collect();
+        let mut piped_commands: Vec<(RefinedJob, JobKind)> = {
+            pipeline.jobs
+                .drain(..)
+                .map(|mut job| {
+                    let refined = {
+                        if self.builtins.contains_key::<str>(
+                            job.command.as_ref()
+                        ) {
+                            RefinedJob::builtin(
+                                job.command,
+                                job.args.drain().collect()
+                            )
+                        } else {
+                            let mut command = Command::new(job.command);
+                            for arg in job.args.drain().skip(1) {
+                                command.arg(arg);
+                            }
+                            RefinedJob::External(command)
+                        }
+                    };
+                    (refined, job.kind)
+                })
+                .collect()
+        };
         match pipeline.stdin {
             None => (),
             Some(Input::File(ref filename)) => {
                 if let Some(command) = piped_commands.first_mut() {
                     match File::open(filename) {
-                        Ok(file) => unsafe {
-                            command.0.stdin(Stdio::from_raw_fd(file.into_raw_fd()));
-                        },
+                        Ok(file) => command.0.stdin(file.into_raw_fd()),
                         Err(e) => {
-                            eprintln!("ion: failed to redirect '{}' into stdin: {}", filename, e);
+                            eprintln!("ion: failed to redirect '{}' into stdin: {}",
+                                      filename, e)
                         }
                     }
                 }
@@ -112,8 +106,11 @@ impl<'a> PipelineExecution for Shell<'a> {
                             command.0.stdin(stdio);
                         },
                         Err(e) => {
-                            eprintln!("ion: failed to redirect herestring '{}' into stdin: {}",
-                                      string, e);
+                            eprintln!(
+                                "ion: failed to redirect herestring '{}' into stdin: {}",
+                                string,
+                                e
+                            );
                         }
                     }
                 }
@@ -128,20 +125,20 @@ impl<'a> PipelineExecution for Shell<'a> {
                     File::create(&stdout.file)
                 };
                 match file {
-                    Ok(f) => unsafe {
-                        match stdout.from {
-                            RedirectFrom::Both => {
-                                let fd = f.into_raw_fd();
-                                command.0.stderr(Stdio::from_raw_fd(fd));
-                                command.0.stdout(Stdio::from_raw_fd(fd));
-                            },
-                            RedirectFrom::Stderr => {
-                                command.0.stderr(Stdio::from_raw_fd(f.into_raw_fd()));
-                            },
-                            RedirectFrom::Stdout => {
-                                command.0.stdout(Stdio::from_raw_fd(f.into_raw_fd()));
-                            },
-                        }
+                    Ok(f) => match stdout.from {
+                        RedirectFrom::Both => {
+                            match f.try_clone() {
+                                Ok(f_copy) => {
+                                    command.0.stdout(f.into_raw_fd());
+                                    command.0.stderr(f_copy.into_raw_fd());
+                                },
+                                Err(e) => {
+                                    eprintln!("ion: failed to redirect both stderr and stdout into file '{:?}': {}", f, e);
+                                }
+                            }
+                        },
+                        RedirectFrom::Stderr => command.0.stderr(f.into_raw_fd()),
+                        RedirectFrom::Stdout => command.0.stdout(f.into_raw_fd()),
                     },
                     Err(err) => {
                         let stderr = io::stderr();
@@ -171,7 +168,7 @@ impl<'a> PipelineExecution for Shell<'a> {
 /// This function will panic if called with an empty slice
 pub fn pipe (
     shell: &mut Shell,
-    commands: Vec<(Command, JobKind)>,
+    commands: Vec<(RefinedJob, JobKind)>,
     foreground: bool
 ) -> i32 {
     let mut previous_status = SUCCESS;
@@ -194,9 +191,10 @@ pub fn pipe (
 
             match kind {
                 JobKind::Pipe(mut mode) => {
-                    // We need to remember the commands as they own the file descriptors that are
-                    // created by create_pipe. We purposfully drop the pipes that are
-                    // owned by a given command in `wait` in order to close those pipes, sending
+                    // We need to remember the commands as they own the file
+                    // descriptors that are created by sys::pipe.
+                    // We purposfully drop the pipes that are owned by a given
+                    // command in `wait` in order to close those pipes, sending
                     // EOF to the next command
                     let mut remember = Vec::new();
                     // A list of the PIDs in the piped command
@@ -205,63 +203,133 @@ pub fn pipe (
                     let mut pgid = 0; // 0 means the PGID is not set yet.
 
                     macro_rules! spawn_proc {
-                        ($cmd:expr) => {{
-                            let child = $cmd.before_exec(move || {
-                                signals::unblock();
-                                create_process_group(pgid);
-                                Ok(())
-                            }).spawn();
-                            match child {
-                                Ok(child) => {
-                                    if pgid == 0 {
-                                        pgid = child.id();
-                                        if foreground {
-                                            let _ = sys::tcsetpgrp(0, pgid);
+                        ($cmd:expr) => {
+                            let short = $cmd.short();
+                            match $cmd {
+                                RefinedJob::External(ref mut command) => {
+                                    match {
+                                        command.before_exec(move || {
+                                            signals::unblock();
+                                            create_process_group(pgid);
+                                            Ok(())
+                                        }).spawn()
+                                    } {
+                                        Ok(child) => {
+                                            if pgid == 0 {
+                                                pgid = child.id();
+                                                if foreground {
+                                                    let _ = sys::tcsetpgrp(0, pgid);
+                                                }
+                                            }
+                                            shell.foreground.push(child.id());
+                                            children.push(child.id());
+                                        },
+                                        Err(e) => {
+                                            eprintln!("ion: failed to spawn `{}`: {}",
+                                                      short, e);
+                                            return NO_SUCH_COMMAND
+                                        }
+                                    }
+                                }
+                                RefinedJob::Builtin { ref name,
+                                                      ref args,
+                                                      ref stdout,
+                                                      ref stderr,
+                                                      ref stdin, } =>
+                                {
+                                    match unsafe { sys::fork() } {
+                                        Ok(0) => {
+                                            signals::unblock();
+                                            create_process_group(pgid);
+                                            let args: Vec<&str> = args
+                                                .iter()
+                                                .map(|x| x as &str).collect();
+                                            builtin(shell,
+                                                    name,
+                                                    &args,
+                                                    *stdout,
+                                                    *stderr,
+                                                    *stdin);
+                                        },
+                                        Ok(pid) => {
+                                            if pgid == 0 {
+                                                pgid = pid;
+                                                if foreground {
+                                                    let _ = sys::tcsetpgrp(0, pgid);
+                                                }
+                                            }
+                                            shell.foreground.push(pid);
+                                            children.push(pid);
+                                        },
+                                        Err(e) => {
+                                            eprintln!("ion: failed to fork {}: {}",
+                                                      short,
+                                                      e);
                                         }
                                     }
-                                    shell.foreground.push(child.id());
-                                    children.push(child.id());
-                                },
-                                Err(e) => {
-                                    eprintln!("ion: failed to spawn `{}`: {}", get_command_name($cmd), e);
-                                    return NO_SUCH_COMMAND
                                 }
                             }
-                        }};
+                        };
                     }
 
                     // Append other jobs until all piped jobs are running
                     while let Some((mut child, ckind)) = commands.next() {
-                        if let Err(e) = unsafe {
-                            create_pipe(&mut parent, &mut child, mode)
-                        } {
-                            eprintln!("ion: failed to create pipe for redirection: {:?}", e);
+                        match sys::pipe2(sys::O_CLOEXEC) {
+                            Err(e) =>  {
+                                eprintln!("ion: failed to create pipe: {:?}", e);
+                            },
+                            Ok((reader, writer)) => {
+                                child.stdin(reader);
+                                match mode {
+                                    RedirectFrom::Stderr => {
+                                        parent.stderr(writer);
+                                    },
+                                    RedirectFrom::Stdout => {
+                                        parent.stdout(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.into_raw_fd());
+                                                parent.stdout(duped.into_raw_fd());
+                                            }
+                                        }
+                                    }
+                                }
+                            }
                         }
-                        spawn_proc!(&mut parent);
+                        spawn_proc!(parent);
                         remember.push(parent);
                         if let JobKind::Pipe(m) = ckind {
                             parent = child;
                             mode = m;
                         } else {
-                            // We set the kind to the last child kind that was processed. For
-                            // example, the pipeline `foo | bar | baz && zardoz` should have the
-                            // previous kind set to `And` after processing the initial pipeline
+                            // We set the kind to the last child kind that was
+                            // processed. For example, the pipeline
+                            // `foo | bar | baz && zardoz` should have the
+                            // previous kind set to `And` after processing the
+                            // initial pipeline
                             kind = ckind;
-                            spawn_proc!(&mut child);
+                            spawn_proc!(child);
                             remember.push(child);
                             break
                         }
                     }
-
                     previous_kind = kind;
                     previous_status = wait(shell, children, remember);
                     if previous_status == TERMINATED {
-                        terminate_fg(shell);
+                        shell.foreground_send(sys::SIGTERM);
                         return previous_status;
                     }
                 }
                 _ => {
-                    previous_status = execute_command(shell, &mut parent, foreground);
+                    previous_status = execute(shell, &mut parent, foreground);
                     previous_kind = kind;
                 }
             }
@@ -272,31 +340,53 @@ pub fn pipe (
     previous_status
 }
 
-fn terminate_fg(shell: &mut Shell) {
-    shell.foreground_send(sys::SIGTERM);
-}
-
-fn execute_command(shell: &mut Shell, command: &mut Command, foreground: bool) -> i32 {
-    match command.before_exec(move || {
-        signals::unblock();
-        create_process_group(0);
-        Ok(())
-    }).spawn() {
-        Ok(child) => {
-            if foreground {
-                let _ = sys::tcsetpgrp(0, child.id());
+fn execute(shell: &mut Shell, job: &mut RefinedJob, foreground: bool) -> i32 {
+    let short = job.short();
+    let long = job.long();
+    match *job {
+        RefinedJob::External(ref mut command) => {
+            match {
+                command.before_exec(move || {
+                    signals::unblock();
+                    create_process_group(0);
+                    Ok(())
+                }).spawn()
+            } {
+                Ok(child) => {
+                    if foreground {
+                        let _ = sys::tcsetpgrp(0, child.id());
+                    }
+                    shell.watch_foreground(child.id(), child.id(), move || long, |_| ())
+                },
+                Err(e) => {
+                    if e.kind() == io::ErrorKind::NotFound {
+                        eprintln!("ion: command not found: {}", short)
+                    } else {
+                        eprintln!("ion: error spawning process: {}", e)
+                    };
+                    FAILURE
+                }
+            }
+        }
+        RefinedJob::Builtin { ref name, ref args, ref stdin, ref stdout, ref stderr } => {
+            match unsafe { sys::fork() } {
+                Ok(0) => {
+                    signals::unblock();
+                    create_process_group(0);
+                    let args: Vec<&str> = args.iter().map(|x| x as &str).collect();
+                    builtin(shell, name, &args, *stdout, *stderr, *stdin)
+                },
+                Ok(pid) => {
+                    if foreground {
+                        let _ = sys::tcsetpgrp(0, pid);
+                    }
+                    shell.watch_foreground(pid, pid, move || long, |_| ())
+                },
+                Err(e) => {
+                    eprintln!("ion: fork error for '{}': {}", short, e);
+                    FAILURE
+                }
             }
-            shell.watch_foreground(child.id(), child.id(), || get_full_command(command), |_| ())
-        },
-        Err(e) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = if e.kind() == io::ErrorKind::NotFound {
-                writeln!(stderr, "ion: Command not found: {}", get_command_name(command))
-            } else {
-                writeln!(stderr, "ion: Error spawning process: {}", e)
-            };
-            FAILURE
         }
     }
 }
@@ -306,14 +396,17 @@ fn execute_command(shell: &mut Shell, command: &mut Command, foreground: bool) -
 fn wait (
     shell: &mut Shell,
     mut children: Vec<u32>,
-    mut commands: Vec<Command>
+    mut commands: Vec<RefinedJob>
 ) -> i32 {
     // TODO: Find a way to only do this when absolutely necessary.
-    let as_string = commands.iter().map(get_full_command)
-            .collect::<Vec<String>>().join(" | ");
+    let as_string = commands.iter()
+        .map(RefinedJob::long)
+        .collect::<Vec<String>>()
+        .join(" | ");
 
     // Each process in the pipe has the same PGID, which is the first process's PID.
     let pgid = children[0];
+
     // If the last process exits, we know that all processes should exit.
     let last_pid = children[children.len()-1];
 
@@ -326,22 +419,47 @@ fn wait (
     })
 }
 
-fn get_command_name(command: &Command) -> String {
-    format!("{:?}", command).split('"').nth(1).unwrap_or("").to_string()
-}
 
-fn get_full_command(command: &Command) -> String {
-    let command = format!("{:?}", command);
-    let mut arg_iter = command.split_whitespace();
-    let command = arg_iter.next().unwrap();
-    let mut output = String::from(&command[1..command.len()-1]);
-    for argument in arg_iter {
-        output.push(' ');
-        if argument.len() > 2 {
-            output.push_str(&argument[1..argument.len()-1]);
-        } else {
-            output.push_str(&argument);
+/// Execute a builtin in the current process. Note that this will exit
+/// the current process with the return code of the builtin
+/// # Args
+/// * `shell`: A `Shell` that forwards relevant information to the builtin
+/// * `name`: Name of the builtin to execute.
+/// * `stdin`, `stdout`, `stderr`: File descriptors that will replace the
+///    respective standard streams if they are not `None`
+/// # Preconditions
+/// * `shell.builtins.contains_key(name)`; otherwise this function will panic
+fn builtin(
+    shell: &mut Shell,
+    name: &str,
+    args: &[&str],
+    stdout: Option<RawFd>,
+    stderr: Option<RawFd>,
+    stdin: Option<RawFd>,
+) -> ! {
+    /// Close a file descriptor by opening a `File` and letting it drop
+    fn close(fd: Option<RawFd>) {
+        if let Some(fd) = fd {
+            if let Err(e) = sys::close(fd) {
+                eprintln!("ion: failed to close file '{}': {}", fd, e);
+            }
         }
     }
-    output
+    if let Some(fd) = stdin {
+        redir(fd, sys::STDIN_FILENO);
+    }
+    if let Some(fd) = stdout {
+        redir(fd, sys::STDOUT_FILENO);
+    }
+    if let Some(fd) = stderr {
+        redir(fd, sys::STDERR_FILENO);
+    }
+    // The precondition for this function asserts that there exists some `builtin`
+    // in `shell` named `name`, so we unwrap here
+    let builtin = shell.builtins.get(name).unwrap();
+    let ret = (builtin.main)(args, shell);
+    close(stderr);
+    close(stdout);
+    close(stdin);
+    exit(ret)
 }
diff --git a/src/sys/redox.rs b/src/sys/redox.rs
index 2b891449a34df18c4ff2f7b9139f99c369e3137d..6aafefab001aee6fd10cb24b06339816cf5092a7 100644
--- a/src/sys/redox.rs
+++ b/src/sys/redox.rs
@@ -15,6 +15,12 @@ pub const SIGCONT: i32 = syscall::SIGCONT as i32;
 pub const SIGSTOP: i32 = syscall::SIGSTOP as i32;
 pub const SIGTSTP: i32 = syscall::SIGTSTP as i32;
 
+/// XXX: These values were infered from the Redox port of `newlib` and may be
+/// sensitive to changes in redox;
+pub const STDIN_FILENO: i32 = 0;
+pub const STDOUT_FILENO: i32 = 1;
+pub const STDERR_FILENO: i32 = 2;
+
 pub unsafe fn fork() -> io::Result<u32> {
     cvt(syscall::clone(0)).map(|pid| pid as u32)
 }
@@ -66,6 +72,14 @@ pub fn tcsetpgrp(tty_fd: RawFd, pgid: u32) -> io::Result<()> {
     cvt(res).and(Ok(()))
 }
 
+pub fn dup2(old: RawFd, new: RawFd) -> io::Result<RawFd> {
+    syscall::call::dup2(old, new, &[]).map_err(|e| io::Error::from_raw_os_err(err.errno))
+}
+
+pub fn close(fd: RawFd) -> io::Result<()> {
+    cvt(syscall::call::close(fd)).and(Ok())
+}
+
 // Support function for converting syscall error to io error
 fn cvt(result: Result<usize, syscall::Error>) -> io::Result<usize> {
     result.map_err(|err| io::Error::from_raw_os_error(err.errno))
diff --git a/src/sys/unix.rs b/src/sys/unix.rs
index aee4ced26269e181067280f68f52a7128f930a8f..c6cd3e2125827b5c252d77a3f17eb583c94063c2 100644
--- a/src/sys/unix.rs
+++ b/src/sys/unix.rs
@@ -14,6 +14,10 @@ pub const SIGCONT: i32 = libc::SIGCONT;
 pub const SIGSTOP: i32 = libc::SIGSTOP;
 pub const SIGTSTP: i32 = libc::SIGTSTP;
 
+pub const STDOUT_FILENO: i32 = libc::STDOUT_FILENO;
+pub const STDERR_FILENO: i32 = libc::STDERR_FILENO;
+pub const STDIN_FILENO: i32 = libc::STDIN_FILENO;
+
 pub unsafe fn fork() -> io::Result<u32> {
     cvt(libc::fork()).map(|pid| pid as u32)
 }
@@ -58,6 +62,14 @@ pub fn tcsetpgrp(fd: RawFd, pgrp: u32) -> io::Result<()> {
     cvt(unsafe { libc::tcsetpgrp(fd as c_int, pgrp as pid_t) }).and(Ok(()))
 }
 
+pub fn dup2(old: RawFd, new: RawFd) -> io::Result<RawFd> {
+    cvt(unsafe { libc::dup2(old, new) })
+}
+
+pub fn close(fd: RawFd) -> io::Result<()> {
+    cvt(unsafe { libc::close(fd) }).and(Ok(()))
+}
+
 // Support functions for converting libc return values to io errors {
 trait IsMinusOne {
     fn is_minus_one(&self) -> bool;
@@ -136,7 +148,7 @@ pub mod job_control {
     use shell::Shell;
     use libc::{self, pid_t};
 
-    use nix::sys::wait::{waitpid, WaitStatus, WNOHANG, WUNTRACED};
+    use nix::sys::wait::{waitpid, wait, WaitStatus, WNOHANG, WUNTRACED};
     #[cfg(not(target_os = "macos"))]
     use nix::sys::wait::{WCONTINUED};
 
@@ -213,7 +225,7 @@ pub mod job_control {
 
     pub fn watch_foreground<'a, F, D>(
         shell: &mut Shell<'a>,
-        pid: u32,
+        _pid: u32,
         last_pid: u32,
         get_command: F,
         mut drop_command: D,
@@ -224,7 +236,7 @@ pub mod job_control {
     {
         let mut exit_status = 0;
         loop {
-            match waitpid(-(pid as pid_t), Some(WUNTRACED)) {
+            match wait() {
                 Ok(WaitStatus::Exited(pid, status)) => if pid == (last_pid as i32) {
                     break status as i32;
                 } else {