diff --git a/src/shell/pipe_exec/command_not_found.rs b/src/shell/pipe_exec/command_not_found.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cd5c975c0c951e6f1e60905964acd1a80a4910f5
--- /dev/null
+++ b/src/shell/pipe_exec/command_not_found.rs
@@ -0,0 +1,35 @@
+use super::super::{Capture, Function, Shell};
+use parser::shell_expand::expand_string;
+use std::io::Read;
+use std::process;
+use sys;
+
+pub(crate) fn command_not_found(shell: &mut Shell, command: &str) -> Option<String> {
+    let function = match shell.functions.get("COMMAND_NOT_FOUND") {
+        Some(func) => func as *const Function,
+        None => return None // TODO: Use ? on Option whenever we drop support for older rust versions
+    };
+
+    let mut output = None;
+
+    match shell.fork(Capture::Stdout, |child| unsafe {
+        let _ = function.read().execute(child, &["ion", command]);
+    }) {
+        Ok(result) => {
+            let mut string = String::new();
+            match result.stdout.unwrap().read_to_string(&mut string) {
+                Ok(_) => output = Some(string),
+                Err(err) => {
+                    eprintln!("ion: error reading stdout of child: {}", err);
+                }
+            }
+        },
+        Err(err) => {
+            eprintln!("ion: fork error: {}", err);
+        }
+    }
+
+    // Ensure that the parent retains ownership of the terminal before exiting.
+    let _ = sys::tcsetpgrp(sys::STDIN_FILENO, process::id());
+    output
+}
diff --git a/src/shell/pipe_exec/mod.rs b/src/shell/pipe_exec/mod.rs
index 44123729583b639c76eb3ff1e4b3b5516ec193ae..88138508b32d50e02faf84a1d041f4f7cb0bbe64 100644
--- a/src/shell/pipe_exec/mod.rs
+++ b/src/shell/pipe_exec/mod.rs
@@ -4,11 +4,13 @@
 //! IDs, watching foreground and background tasks, sending foreground tasks to the background,
 //! handling pipeline and conditional operators, and std{in,out,err} redirections.
 
+mod command_not_found;
 pub mod foreground;
 mod fork;
 pub mod job_control;
 mod streams;
 
+use self::command_not_found::command_not_found;
 use self::fork::{create_process_group, fork_pipe};
 use self::job_control::JobControl;
 use self::streams::{duplicate_streams, redir, redirect_streams};
@@ -495,7 +497,11 @@ impl PipelineExecution for Shell {
                     self.watch_foreground(child.id(), child.id(), move || long, |_| ())
                 }
                 Err(e) => if e.kind() == io::ErrorKind::NotFound {
-                    eprintln!("ion: command not found: {}", short);
+                    if let Some(output) = command_not_found(self, &short) {
+                        print!("{}", output);
+                    } else {
+                        eprintln!("ion: command not found: {}", short);
+                    }
                     NO_SUCH_COMMAND
                 } else {
                     eprintln!("ion: error spawning process: {}", e);
@@ -749,7 +755,11 @@ pub(crate) fn pipe(
                                         },
                                         Err(e) => {
                                             return if e.kind() == io::ErrorKind::NotFound {
-                                                eprintln!("ion: command not found: {}", short);
+                                                if let Some(output) = command_not_found(shell, &short) {
+                                                    print!("{}", output);
+                                                } else {
+                                                    eprintln!("ion: command not found: {}", short);
+                                                }
                                                 NO_SUCH_COMMAND
                                             } else {
                                                 eprintln!("ion: error spawning process: {}", e);