diff --git a/src/binary/builtins.rs b/src/binary/builtins.rs
new file mode 100644
index 0000000000000000000000000000000000000000..52b4986223f5ef9592445355aa402989c0f73c6f
--- /dev/null
+++ b/src/binary/builtins.rs
@@ -0,0 +1,71 @@
+use ion_shell::{builtins::man_pages::check_help, status::Status, types::Str, Shell};
+use ion_sys::{execve, SIGTERM};
+use std::error::Error;
+
+const MAN_EXEC: &str = r#"NAME
+    exec - Replace the shell with the given command.
+
+SYNOPSIS
+    exec [-ch] [--help] [command [arguments ...]]
+
+DESCRIPTION
+    Execute <command>, replacing the shell with the specified program.
+    The <arguments> following the command become the arguments to
+    <command>.
+
+OPTIONS
+    -c  Execute command with an empty environment."#;
+
+pub const MAN_EXIT: &str = r#"NAME
+    exit - exit the shell
+
+SYNOPSIS
+    exit
+
+DESCRIPTION
+    Makes ion exit. The exit status will be that of the last command executed."#;
+
+/// Executes the givent commmand.
+pub fn _exec(args: &[small::String]) -> Result<(), small::String> {
+    let mut clear_env = false;
+    let mut idx = 0;
+    for arg in args.iter() {
+        match &**arg {
+            "-c" => clear_env = true,
+            _ if check_help(args, MAN_EXEC) => {
+                return Ok(());
+            }
+            _ => break,
+        }
+        idx += 1;
+    }
+
+    match args.get(idx) {
+        Some(argument) => {
+            let args = if args.len() > idx + 1 { &args[idx + 1..] } else { &[] };
+            Err(execve(argument, args, clear_env).description().into())
+        }
+        None => Err("no command provided".into()),
+    }
+}
+
+pub fn exit(args: &[Str], shell: &mut Shell<'_>) -> Status {
+    if check_help(args, MAN_EXIT) {
+        return Status::SUCCESS;
+    }
+    // Kill all active background tasks before exiting the shell.
+    shell.background_send(SIGTERM);
+    let exit_code = args
+        .get(1)
+        .and_then(|status| status.parse::<i32>().ok())
+        .unwrap_or_else(|| shell.previous_status().as_os_code());
+    std::process::exit(exit_code);
+}
+
+pub fn exec(args: &[Str], _shell: &mut Shell<'_>) -> Status {
+    match _exec(&args[1..]) {
+        // Shouldn't ever hit this case.
+        Ok(()) => unreachable!(),
+        Err(err) => Status::error(format!("ion: exec: {}", err)),
+    }
+}
diff --git a/src/binary/mod.rs b/src/binary/mod.rs
index acebf190ef053b469fef2044e323e2a25b4ca9c6..ec97e22feaf25c1ee5ad21bece007c2e21bc0103 100644
--- a/src/binary/mod.rs
+++ b/src/binary/mod.rs
@@ -1,11 +1,11 @@
 //! Contains the binary logic of Ion.
+pub mod builtins;
 mod completer;
 mod designators;
 mod history;
 mod prompt;
 mod readln;
 
-use self::{prompt::prompt, readln::readln};
 use ion_shell::{
     builtins::man_pages,
     parser::{Expander, Terminator},
@@ -162,29 +162,23 @@ impl<'a> InteractiveBinary<'a> {
 
         // change the lifetime to allow adding local builtins
         let InteractiveBinary { context, shell } = self;
-        let this = InteractiveBinary { context, shell: RefCell::new(shell.into_inner()) };
-
-        this.shell.borrow_mut().builtins_mut().add(
-            "history",
-            history,
-            "Display a log of all commands previously executed",
-        );
-        this.shell.borrow_mut().builtins_mut().add(
-            "keybindings",
-            keybindings,
-            "Change the keybindings",
-        );
-        this.shell.borrow_mut().builtins_mut().add("exit", exit, "Exits the current session");
-        this.shell.borrow_mut().builtins_mut().add(
-            "exec",
-            exec,
-            "Replace the shell with the given command.",
-        );
+        let mut shell = shell.into_inner();
+        let builtins = shell.builtins_mut();
+        builtins.add("history", history, "Display a log of all commands previously executed");
+        builtins.add("keybindings", keybindings, "Change the keybindings");
+        builtins.add("exit", exit, "Exits the current session");
+        builtins.add("exec", exec, "Replace the shell with the given command.");
 
+        Self::exec_init_file(&mut shell);
+
+        InteractiveBinary { context, shell: RefCell::new(shell) }.exec(prep_for_exit)
+    }
+
+    fn exec_init_file(shell: &mut Shell) {
         match BaseDirectories::with_prefix("ion") {
             Ok(base_dirs) => match base_dirs.find_config_file(Self::CONFIG_FILE_NAME) {
                 Some(initrc) => {
-                    if let Err(err) = this.shell.borrow_mut().execute_file(&initrc) {
+                    if let Err(err) = shell.execute_file(&initrc) {
                         eprintln!("ion: {}", err)
                     }
                 }
@@ -198,46 +192,36 @@ impl<'a> InteractiveBinary<'a> {
                 eprintln!("ion: unable to get base directory: {}", err);
             }
         }
+    }
 
+    fn exec<T: Fn(&mut Shell<'_>)>(self, prep_for_exit: &T) -> ! {
         loop {
-            let mut lines = std::iter::repeat_with(|| this.readln(prep_for_exit))
+            let mut lines = std::iter::repeat_with(|| self.readln(prep_for_exit))
                 .filter_map(|cmd| cmd)
                 .flat_map(|s| s.into_bytes().into_iter().chain(Some(b'\n')));
             match Terminator::new(&mut lines).terminate() {
                 Some(command) => {
-                    this.shell.borrow_mut().unterminated = false;
+                    self.shell.borrow_mut().unterminated = false;
                     let cmd: &str = &designators::expand_designators(
-                        &this.context.borrow(),
+                        &self.context.borrow(),
                         command.trim_end(),
                     );
-                    if let Err(why) = this.shell.borrow_mut().on_command(&cmd) {
+                    if let Err(why) = self.shell.borrow_mut().on_command(&cmd) {
                         eprintln!("{}", why);
                     }
-                    this.save_command(&cmd);
+                    self.save_command(&cmd);
                 }
                 None => {
-                    this.shell.borrow_mut().unterminated = true;
+                    self.shell.borrow_mut().unterminated = true;
                 }
             }
         }
     }
 
     /// Set the keybindings of the underlying liner context
-    #[inline]
     pub fn set_keybindings(&mut self, key_bindings: KeyBindings) {
         self.context.borrow_mut().key_bindings = key_bindings;
     }
-
-    /// Ion's interface to Liner's `read_line` method, which handles everything related to
-    /// rendering, controlling, and getting input from the prompt.
-    #[inline]
-    pub fn readln<T: Fn(&mut Shell<'_>)>(&self, prep_for_exit: &T) -> Option<String> {
-        readln(self, prep_for_exit)
-    }
-
-    /// Generates the prompt that will be used by Liner.
-    #[inline]
-    pub fn prompt(&self) -> String { prompt(&self.shell.borrow_mut()) }
 }
 
 #[derive(Debug)]
diff --git a/src/binary/prompt.rs b/src/binary/prompt.rs
index d2a9f9c6f682a9981c78b4416c332cc9e6261622..e1bf5628b816dac806782f822dac95676edc6f05 100644
--- a/src/binary/prompt.rs
+++ b/src/binary/prompt.rs
@@ -1,37 +1,42 @@
+use super::InteractiveBinary;
 use ion_shell::{parser::Expander, Capture, Shell};
 use std::io::Read;
 
-pub fn prompt(shell: &Shell<'_>) -> String {
-    let blocks = shell.block_len() + if shell.unterminated { 1 } else { 0 };
+impl<'a> InteractiveBinary<'a> {
+    /// Generates the prompt that will be used by Liner.
+    pub fn prompt(&self) -> String {
+        let shell = self.shell.borrow();
+        let blocks = shell.block_len() + if shell.unterminated { 1 } else { 0 };
 
-    if blocks == 0 {
-        prompt_fn(&shell).unwrap_or_else(|| {
-            shell
-                .get_string(&shell.variables().get_str("PROMPT").unwrap_or_default())
-                .as_str()
-                .into()
-        })
-    } else {
-        "    ".repeat(blocks)
+        if blocks == 0 {
+            Self::prompt_fn(&shell).unwrap_or_else(|| {
+                shell
+                    .get_string(&shell.variables().get_str("PROMPT").unwrap_or_default())
+                    .as_str()
+                    .into()
+            })
+        } else {
+            "    ".repeat(blocks)
+        }
     }
-}
 
-pub fn prompt_fn(shell: &Shell<'_>) -> Option<String> {
-    shell
-        .fork_function(
-            Capture::StdoutThenIgnoreStderr,
-            |result| {
-                let mut string = String::with_capacity(1024);
-                match result.stdout.ok_or(())?.read_to_string(&mut string) {
-                    Ok(_) => Ok(string),
-                    Err(why) => {
-                        eprintln!("ion: error reading stdout of child: {}", why);
-                        Err(())
+    pub fn prompt_fn(shell: &Shell<'_>) -> Option<String> {
+        shell
+            .fork_function(
+                Capture::StdoutThenIgnoreStderr,
+                |result| {
+                    let mut string = String::with_capacity(1024);
+                    match result.stdout.ok_or(())?.read_to_string(&mut string) {
+                        Ok(_) => Ok(string),
+                        Err(why) => {
+                            eprintln!("ion: error reading stdout of child: {}", why);
+                            Err(())
+                        }
                     }
-                }
-            },
-            "PROMPT",
-            &["ion"],
-        )
-        .ok()
+                },
+                "PROMPT",
+                &["ion"],
+            )
+            .ok()
+    }
 }
diff --git a/src/binary/readln.rs b/src/binary/readln.rs
index 80b4df732bd8ce3e4a52422d4eb15cfa980f779c..07ee3260f99bb3e4edd9c6bb0f0094cbd5a141e0 100644
--- a/src/binary/readln.rs
+++ b/src/binary/readln.rs
@@ -2,38 +2,41 @@ use super::{completer::IonCompleter, InteractiveBinary};
 use ion_shell::Shell;
 use std::io::ErrorKind;
 
-pub fn readln<T: Fn(&mut Shell<'_>)>(
-    binary: &InteractiveBinary<'_>,
-    prep_for_exit: &T,
-) -> Option<String> {
-    let prompt = binary.prompt();
-    let line = binary.context.borrow_mut().read_line(
-        prompt,
-        None,
-        &mut IonCompleter::new(&binary.shell.borrow()),
-    );
+impl<'a> InteractiveBinary<'a> {
+    /// Ion's interface to Liner's `read_line` method, which handles everything related to
+    /// rendering, controlling, and getting input from the prompt.
+    pub fn readln<T: Fn(&mut Shell<'_>)>(&self, prep_for_exit: &T) -> Option<String> {
+        let prompt = self.prompt();
+        let line = self.context.borrow_mut().read_line(
+            prompt,
+            None,
+            &mut IonCompleter::new(&self.shell.borrow()),
+        );
 
-    match line {
-        Ok(line) => {
-            if line.bytes().next() != Some(b'#') && line.bytes().any(|c| !c.is_ascii_whitespace()) {
-                binary.shell.borrow_mut().unterminated = true;
+        match line {
+            Ok(line) => {
+                if line.bytes().next() != Some(b'#')
+                    && line.bytes().any(|c| !c.is_ascii_whitespace())
+                {
+                    self.shell.borrow_mut().unterminated = true;
+                }
+                Some(line)
             }
-            Some(line)
-        }
-        // Handles Ctrl + C
-        Err(ref err) if err.kind() == ErrorKind::Interrupted => None,
-        // Handles Ctrl + D
-        Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => {
-            let mut shell = binary.shell.borrow_mut();
-            if !shell.unterminated && shell.exit_block().is_err() {
-                prep_for_exit(&mut shell);
-                std::process::exit(shell.previous_status().as_os_code())
+            // Handles Ctrl + C
+            Err(ref err) if err.kind() == ErrorKind::Interrupted => None,
+            // Handles Ctrl + D
+            Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => {
+                let mut shell = self.shell.borrow_mut();
+                if !shell.unterminated && shell.exit_block().is_err() {
+                    prep_for_exit(&mut shell);
+                    std::process::exit(shell.previous_status().as_os_code())
+                }
+                None
+            }
+            Err(err) => {
+                eprintln!("ion: liner: {}", err);
+                None
             }
-            None
-        }
-        Err(err) => {
-            eprintln!("ion: liner: {}", err);
-            None
         }
     }
 }
diff --git a/src/main.rs b/src/main.rs
index f779dfa85d49564d5c1e3e6e55a45bc74bf4c570..ff62e8e66905820b80f83c23599fee6435186ec4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,18 +1,12 @@
 mod binary;
 
-use self::binary::{InteractiveBinary, MAN_ION};
-use ion_shell::{
-    builtins::man_pages::check_help, status::Status, types::Str, BuiltinMap, IonError,
-    PipelineError, Shell, Value,
-};
+use self::binary::{builtins, InteractiveBinary, MAN_ION};
+use ion_shell::{BuiltinMap, IonError, PipelineError, Shell, Value};
 use ion_sys as sys;
-use ion_sys::execve;
 use liner::KeyBindings;
-use small;
 use std::{
     alloc::System,
     env,
-    error::Error,
     io::{self, stdin, BufReader},
     process,
 };
@@ -26,78 +20,10 @@ fn set_unique_pid() -> io::Result<()> {
     sys::tcsetpgrp(0, pid)
 }
 
-const MAN_EXEC: &str = r#"NAME
-    exec - Replace the shell with the given command.
-
-SYNOPSIS
-    exec [-ch] [--help] [command [arguments ...]]
-
-DESCRIPTION
-    Execute <command>, replacing the shell with the specified program.
-    The <arguments> following the command become the arguments to
-    <command>.
-
-OPTIONS
-    -c  Execute command with an empty environment."#;
-
-pub const MAN_EXIT: &str = r#"NAME
-    exit - exit the shell
-
-SYNOPSIS
-    exit
-
-DESCRIPTION
-    Makes ion exit. The exit status will be that of the last command executed."#;
-
-/// Executes the givent commmand.
-pub fn exec(args: &[small::String]) -> Result<(), small::String> {
-    let mut clear_env = false;
-    let mut idx = 0;
-    for arg in args.iter() {
-        match &**arg {
-            "-c" => clear_env = true,
-            _ if check_help(args, MAN_EXEC) => {
-                return Ok(());
-            }
-            _ => break,
-        }
-        idx += 1;
-    }
-
-    match args.get(idx) {
-        Some(argument) => {
-            let args = if args.len() > idx + 1 { &args[idx + 1..] } else { &[] };
-            Err(execve(argument, args, clear_env).description().into())
-        }
-        None => Err("no command provided".into()),
-    }
-}
-
-fn builtin_exit(args: &[Str], shell: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_EXIT) {
-        return Status::SUCCESS;
-    }
-    // Kill all active background tasks before exiting the shell.
-    shell.background_send(sys::SIGTERM);
-    let exit_code = args
-        .get(1)
-        .and_then(|status| status.parse::<i32>().ok())
-        .unwrap_or_else(|| shell.previous_status().as_os_code());
-    std::process::exit(exit_code);
-}
-
-fn builtin_exec(args: &[Str], _shell: &mut Shell<'_>) -> Status {
-    match exec(&args[1..]) {
-        // Shouldn't ever hit this case.
-        Ok(()) => unreachable!(),
-        Err(err) => Status::error(format!("ion: exec: {}", err)),
-    }
-}
-
 fn main() {
     let mut builtins = BuiltinMap::default().with_shell_unsafe();
-    builtins.add("exec", &builtin_exec, "Replace the shell with the given command.");
-    builtins.add("exit", &builtin_exit, "Exits the current session");
+    builtins.add("exec", &builtins::exec, "Replace the shell with the given command.");
+    builtins.add("exit", &builtins::exit, "Exits the current session");
 
     let stdin_is_a_tty = sys::isatty(sys::STDIN_FILENO);
     let mut shell = Shell::with_builtins(builtins, false);