diff --git a/src/lib/builtins/mod.rs b/src/lib/builtins/mod.rs
index d1d5d85b90b56ea21948c53394434028a7df28c6..57c644debb2826c243cb3ae9c7862b0a7c789a37 100644
--- a/src/lib/builtins/mod.rs
+++ b/src/lib/builtins/mod.rs
@@ -28,8 +28,10 @@ use self::{
 };
 
 use std::{
+    borrow::Cow,
     error::Error,
     io::{self, BufRead, Write},
+    path::PathBuf,
 };
 
 use hashbrown::HashMap;
@@ -39,6 +41,7 @@ use crate::{
     shell::{self, status::*, ProcessState, Shell},
     sys, types,
 };
+use itertools::Itertools;
 use small;
 
 const HELP_DESC: &str = "Display helpful information about a given command or list commands if \
@@ -61,6 +64,17 @@ macro_rules! map {
     }};
 }
 
+// parses -N or +N patterns
+// required for popd, pushd, dirs
+fn parse_numeric_arg(arg: &str) -> Option<(bool, usize)> {
+    match arg.chars().nth(0) {
+        Some('+') => Some(true),
+        Some('-') => Some(false),
+        _ => None,
+    }
+    .and_then(|b| arg[1..].parse::<usize>().ok().map(|num| (b, num)))
+}
+
 /// A container for builtins and their respective help text
 ///
 /// Note: To reduce allocations, function are provided as pointer rather than boxed closures
@@ -308,41 +322,214 @@ fn builtin_is(args: &[small::String], shell: &mut Shell) -> i32 {
 }
 
 fn builtin_dirs(args: &[small::String], shell: &mut Shell) -> i32 {
+    // converts pbuf to an absolute path if possible
+    fn try_abs_path(pbuf: &PathBuf) -> Cow<str> {
+        Cow::Owned(
+            pbuf.canonicalize().unwrap_or_else(|_| pbuf.clone()).to_string_lossy().to_string(),
+        )
+    }
+
     if check_help(args, MAN_DIRS) {
         return SUCCESS;
     }
 
-    shell.dir_stack(args.iter().skip(1))
+    let mut clear = false; // -c
+    let mut abs_pathnames = false; // -l
+    let mut multiline = false; // -p | -v
+    let mut index = false; // -v
+
+    let mut num_arg = None;
+
+    for arg in args.iter().skip(1) {
+        match arg.as_ref() {
+            "-c" => clear = true,
+            "-l" => abs_pathnames = true,
+            "-p" => multiline = true,
+            "-v" => {
+                index = true;
+                multiline = true;
+            }
+            _ => num_arg = Some(arg),
+        }
+    }
+
+    if clear {
+        shell.clear_dir_stack();
+    }
+
+    let mapper: fn((usize, &PathBuf)) -> Cow<str> = match (abs_pathnames, index) {
+        // ABS, INDEX
+        (true, true) => |(num, x)| Cow::Owned(format!(" {}  {}", num, try_abs_path(x))),
+        (true, false) => |(_, x)| try_abs_path(x),
+        (false, true) => |(num, x)| Cow::Owned(format!(" {}  {}", num, x.to_string_lossy())),
+        (false, false) => |(_, x)| x.to_string_lossy(),
+    };
+
+    let mut iter = shell.dir_stack().enumerate().map(mapper);
+
+    if let Some(arg) = num_arg {
+        let num = match parse_numeric_arg(arg.as_ref()) {
+            Some((true, num)) => num,
+            Some((false, num)) if shell.dir_stack().count() > num => {
+                shell.dir_stack().count() - num - 1
+            }
+            _ => return FAILURE, /* Err(Cow::Owned(format!("ion: dirs: {}: invalid
+                                  * argument\n", arg))) */
+        };
+        match iter.nth(num) {
+            Some(x) => {
+                println!("{}", x);
+                SUCCESS
+            }
+            None => FAILURE,
+        }
+    } else {
+        let folder: fn(String, Cow<str>) -> String =
+            if multiline { |x, y| x + "\n" + &y } else { |x, y| x + " " + &y };
+
+        if let Some(x) = iter.next() {
+            println!("{}", iter.fold(x.to_string(), folder));
+        }
+        SUCCESS
+    }
 }
 
 fn builtin_pushd(args: &[small::String], shell: &mut Shell) -> i32 {
     if check_help(args, MAN_PUSHD) {
         return SUCCESS;
     }
-    match shell.pushd(args.iter().skip(1)) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr.write_all(why.as_bytes());
-            FAILURE
+
+    enum Action {
+        Switch,          // <no arguments>
+        RotLeft(usize),  // +[num]
+        RotRight(usize), // -[num]
+        Push(PathBuf),   // [dir]
+    }
+
+    let mut keep_front = false; // whether the -n option is present
+    let mut action = Action::Switch;
+
+    for arg in args.iter().skip(1) {
+        let arg = arg.as_ref();
+        if arg == "-n" {
+            keep_front = true;
+        } else if let Action::Switch = action {
+            // if action is not yet defined
+            action = match parse_numeric_arg(arg) {
+                Some((true, num)) => Action::RotLeft(num),
+                Some((false, num)) => Action::RotRight(num),
+                None => Action::Push(PathBuf::from(arg)), // no numeric arg => `dir`-parameter
+            };
+        } else {
+            eprintln!("ion: pushd: too many arguments");
+            return FAILURE;
         }
     }
+
+    match action {
+        Action::Switch => {
+            if !keep_front {
+                if let Err(why) = shell.swap(1) {
+                    eprintln!("ion: pushd: {}", why);
+                    return FAILURE;
+                }
+            }
+        }
+        Action::RotLeft(num) => {
+            if !keep_front {
+                if let Err(why) = shell.rotate_left(num) {
+                    eprintln!("ion: pushd: {}", why);
+                    return FAILURE;
+                }
+            }
+        }
+        Action::RotRight(num) => {
+            if !keep_front {
+                if let Err(why) = shell.rotate_right(num) {
+                    eprintln!("ion: pushd: {}", why);
+                    return FAILURE;
+                }
+            }
+        }
+        Action::Push(dir) => {
+            if let Err(why) = shell.pushd(dir, keep_front) {
+                eprintln!("ion: pushd: {}", why);
+                return FAILURE;
+            }
+        }
+    };
+
+    println!(
+        "{}",
+        shell.dir_stack().map(|dir| dir.to_str().unwrap_or("ion: no directory found")).join(" ")
+    );
+    SUCCESS
 }
 
 fn builtin_popd(args: &[small::String], shell: &mut Shell) -> i32 {
     if check_help(args, MAN_POPD) {
         return SUCCESS;
     }
-    match shell.popd(args.iter().skip(1)) {
-        Ok(()) => SUCCESS,
-        Err(why) => {
-            let stderr = io::stderr();
-            let mut stderr = stderr.lock();
-            let _ = stderr.write_all(why.as_bytes());
-            FAILURE
+
+    let len = shell.dir_stack().len();
+    if len <= 1 {
+        eprintln!("ion: popd: directory stack empty");
+        return FAILURE;
+    }
+
+    let mut keep_front = false; // whether the -n option is present
+    let mut index: usize = 0;
+
+    for arg in args.iter().skip(1) {
+        let arg = arg.as_ref();
+        if arg == "-n" {
+            keep_front = true;
+        } else {
+            let (count_from_front, num) = match parse_numeric_arg(arg) {
+                Some(n) => n,
+                None => {
+                    eprintln!("ion: popd: {}: invalid argument", arg);
+                    return FAILURE;
+                }
+            };
+
+            index = if count_from_front {
+                // <=> input number is positive
+                num
+            } else if let Some(n) = (len - 1).checked_sub(num) {
+                n
+            } else {
+                eprintln!("ion: popd: negative directory stack index out of range");
+                return FAILURE;
+            };
         }
     }
+
+    // apply -n
+    if index == 0 && keep_front {
+        index = 1;
+    } else if index == 0 {
+        // change to new directory, return if not possible
+        if let Err(why) = shell.set_current_dir_by_index(1) {
+            eprintln!("ion: popd: {}", why);
+            return FAILURE;
+        }
+    }
+
+    // pop element
+    if shell.popd(index).is_some() {
+        println!(
+            "{}",
+            shell
+                .dir_stack()
+                .map(|dir| dir.to_str().unwrap_or("ion: no directory found"))
+                .join(" ")
+        );
+        SUCCESS
+    } else {
+        eprintln!("ion: popd: {}: directory stack index out of range", index);
+        FAILURE
+    }
 }
 
 fn builtin_alias(args: &[small::String], shell: &mut Shell) -> i32 {
diff --git a/src/lib/lib.rs b/src/lib/lib.rs
index 270e8873be68e34277d8dc3501066106c63fb798..5ff9351307e2db060740a28e13b98e88bfdcc599 100644
--- a/src/lib/lib.rs
+++ b/src/lib/lib.rs
@@ -16,4 +16,7 @@ mod memory;
 mod shell;
 
 pub(crate) use self::memory::IonPool;
-pub use crate::shell::*;
+pub use crate::{
+    builtins::{BuiltinFunction, BuiltinMap},
+    shell::*,
+};
diff --git a/src/lib/shell/directory_stack.rs b/src/lib/shell/directory_stack.rs
index 7c5d09aabab4df13b8595757d8e94dcfb4819006..0b677ff9849fd55b69736e6c4137d78e07f9ca4a 100644
--- a/src/lib/shell/directory_stack.rs
+++ b/src/lib/shell/directory_stack.rs
@@ -1,7 +1,4 @@
-use super::{
-    status::{FAILURE, SUCCESS},
-    variables::{Value, Variables},
-};
+use super::variables::{Value, Variables};
 use crate::sys::env as sys_env;
 use std::{
     borrow::Cow,
@@ -56,103 +53,39 @@ impl DirectoryStack {
     }
 
     // pushd -<num>
-    fn rotate_right(&mut self, num: usize) {
+    pub fn rotate_right(&mut self, num: usize) -> Result<(), Cow<'static, str>> {
         let len = self.dirs.len();
-        self.rotate_left(len - (num % len));
+        self.rotate_left(len - (num % len))
     }
 
     // pushd +<num>
-    fn rotate_left(&mut self, num: usize) {
-        let cloned = self.dirs.clone();
-        for (dest, src) in self.dirs.iter_mut().zip(cloned.iter().cycle().skip(num)) {
-            *dest = src.clone();
+    pub fn rotate_left(&mut self, num: usize) -> Result<(), Cow<'static, str>> {
+        for _ in 0..num {
+            if let Some(popped_front) = self.dirs.pop_front() {
+                self.dirs.push_back(popped_front);
+            }
         }
+        self.set_current_dir_by_index(0)
     }
 
     // sets current_dir to the element referred by index
-    fn set_current_dir_by_index(
-        &self,
-        index: usize,
-        caller: &str,
-    ) -> Result<(), Cow<'static, str>> {
-        let dir = self.dirs.get(index).ok_or_else(|| {
-            Cow::Owned(format!("ion: {}: {}: directory stack out of range", caller, index))
-        })?;
+    pub fn set_current_dir_by_index(&self, index: usize) -> Result<(), Cow<'static, str>> {
+        let dir = self
+            .dirs
+            .get(index)
+            .ok_or_else(|| Cow::Owned(format!("{}: directory stack out of range", index)))?;
 
         set_current_dir_ion(dir)
     }
 
-    fn print_dirs(&self) {
-        let dir = self.dirs.iter().fold(String::new(), |acc, dir| {
-            acc + " " + dir.to_str().unwrap_or("ion: no directory found")
-        });
-        println!("{}", dir.trim_start());
-    }
-
     pub fn dir_from_bottom(&self, num: usize) -> Option<&PathBuf> {
-        self.dirs.iter().rev().nth(num)
+        self.dirs.get(self.dirs.len() - num)
     }
 
     pub fn dir_from_top(&self, num: usize) -> Option<&PathBuf> { self.dirs.get(num) }
 
-    pub fn dirs<I: IntoIterator<Item = T>, T: AsRef<str>>(&mut self, args: I) -> i32 {
-        let mut clear = false; // -c
-        let mut abs_pathnames = false; // -l
-        let mut multiline = false; // -p | -v
-        let mut index = false; // -v
-
-        let mut num_arg = None;
-
-        for arg in args {
-            match arg.as_ref() {
-                "-c" => clear = true,
-                "-l" => abs_pathnames = true,
-                "-p" => multiline = true,
-                "-v" => {
-                    index = true;
-                    multiline = true;
-                }
-                _ => num_arg = Some(arg),
-            }
-        }
-
-        if clear {
-            self.dirs.truncate(1);
-        }
-
-        let mapper: fn((usize, &PathBuf)) -> Cow<str> = match (abs_pathnames, index) {
-            // ABS, INDEX
-            (true, true) => |(num, x)| Cow::Owned(format!(" {}  {}", num, try_abs_path(x))),
-            (true, false) => |(_, x)| try_abs_path(x),
-            (false, true) => |(num, x)| Cow::Owned(format!(" {}  {}", num, x.to_string_lossy())),
-            (false, false) => |(_, x)| x.to_string_lossy(),
-        };
-
-        let mut iter = self.dirs.iter().enumerate().map(mapper);
-
-        if let Some(arg) = num_arg {
-            let num = match parse_numeric_arg(arg.as_ref()) {
-                Some((true, num)) => num,
-                Some((false, num)) if self.dirs.len() > num => self.dirs.len() - num - 1,
-                _ => return FAILURE, /* Err(Cow::Owned(format!("ion: dirs: {}: invalid
-                                      * argument\n", arg))) */
-            };
-            match iter.nth(num) {
-                Some(x) => {
-                    println!("{}", x);
-                    SUCCESS
-                }
-                None => FAILURE,
-            }
-        } else {
-            let folder: fn(String, Cow<str>) -> String =
-                if multiline { |x, y| x + "\n" + &y } else { |x, y| x + " " + &y };
-
-            if let Some(x) = iter.next() {
-                println!("{}", iter.fold(x.to_string(), folder));
-            }
-            SUCCESS
-        }
+    pub fn dirs(&self) -> impl DoubleEndedIterator<Item = &PathBuf> + ExactSizeIterator {
+        self.dirs.iter()
     }
 
     fn insert_dir(&mut self, index: usize, path: PathBuf, variables: &Variables) {
@@ -162,27 +95,24 @@ impl DirectoryStack {
 
     fn push_dir(&mut self, path: PathBuf, variables: &Variables) {
         self.dirs.push_front(path);
-
         self.dirs.truncate(DirectoryStack::get_size(variables));
     }
 
-    pub fn change_and_push_dir(
+    fn change_and_push_dir(
         &mut self,
         dir: &str,
         variables: &Variables,
     ) -> Result<(), Cow<'static, str>> {
         let new_dir = self.normalize_path(dir);
-        match set_current_dir_ion(&new_dir) {
-            Ok(()) => {
-                self.push_dir(new_dir, variables);
-                Ok(())
-            }
-            Err(err) => Err(Cow::Owned(format!(
+        set_current_dir_ion(&new_dir).map_err(|err| {
+            Cow::Owned(format!(
                 "ion: failed to set current dir to {}: {}",
                 new_dir.to_string_lossy(),
                 err
-            ))),
-        }
+            ))
+        })?;
+        self.push_dir(new_dir, variables);
+        Ok(())
     }
 
     fn get_previous_dir(&self) -> Option<String> {
@@ -245,127 +175,31 @@ impl DirectoryStack {
         }
     }
 
-    pub fn pushd<T, I>(
-        &mut self,
-        args: I,
-        variables: &mut Variables,
-    ) -> Result<(), Cow<'static, str>>
-    where
-        T: AsRef<str>,
-        I: IntoIterator<Item = T>,
-    {
-        enum Action {
-            Switch,          // <no arguments>
-            RotLeft(usize),  // +[num]
-            RotRight(usize), // -[num]
-            Push(PathBuf),   // [dir]
-        }
-
-        let mut keep_front = false; // whether the -n option is present
-        let mut action = Action::Switch;
-
-        for arg in args {
-            let arg = arg.as_ref();
-            if arg == "-n" {
-                keep_front = true;
-            } else if let Action::Switch = action {
-                // if action is not yet defined
-                action = match parse_numeric_arg(arg) {
-                    Some((true, num)) => Action::RotLeft(num),
-                    Some((false, num)) => Action::RotRight(num),
-                    None => Action::Push(PathBuf::from(arg)), // no numeric arg => `dir`-parameter
-                };
-            } else {
-                return Err(Cow::Borrowed("ion: pushd: too many arguments"));
-            }
+    pub fn swap(&mut self, index: usize) -> Result<(), Cow<'static, str>> {
+        if self.dirs.len() <= index {
+            return Err(Cow::Borrowed("no other directory"));
         }
-
-        let len = self.dirs.len();
-        match action {
-            Action::Switch => {
-                if len < 2 {
-                    return Err(Cow::Borrowed("ion: pushd: no other directory"));
-                }
-                if !keep_front {
-                    self.set_current_dir_by_index(1, "pushd")?;
-                    self.dirs.swap(0, 1);
-                }
-            }
-            Action::RotLeft(num) => {
-                if !keep_front {
-                    self.set_current_dir_by_index(num, "pushd")?;
-                    self.rotate_left(num);
-                }
-            }
-            Action::RotRight(num) => {
-                if !keep_front {
-                    self.set_current_dir_by_index(len - (num % len), "pushd")?;
-                    self.rotate_right(num);
-                }
-            }
-            Action::Push(dir) => {
-                let index = if keep_front { 1 } else { 0 };
-                let new_dir = self.normalize_path(dir.to_str().unwrap());
-                self.insert_dir(index, new_dir, variables);
-                self.set_current_dir_by_index(index, "pushd")?;
-            }
-        };
-
-        self.print_dirs();
-        Ok(())
+        self.dirs.swap(0, index);
+        self.set_current_dir_by_index(0)
     }
 
-    /// Attempts to set the current directory to the directory stack's previous directory,
-    /// and then removes the front directory from the stack.
-    pub fn popd<T: AsRef<str>, I: IntoIterator<Item = T>>(
+    pub fn pushd(
         &mut self,
-        args: I,
+        path: PathBuf,
+        keep_front: bool,
+        variables: &mut Variables,
     ) -> Result<(), Cow<'static, str>> {
-        let len = self.dirs.len();
-        if len <= 1 {
-            return Err(Cow::Borrowed("ion: popd: directory stack empty"));
-        }
-
-        let mut keep_front = false; // whether the -n option is present
-        let mut index: usize = 0;
-
-        for arg in args {
-            let arg = arg.as_ref();
-            if arg == "-n" {
-                keep_front = true;
-            } else {
-                let (count_from_front, num) = parse_numeric_arg(arg)
-                    .ok_or_else(|| Cow::Owned(format!("ion: popd: {}: invalid argument", arg)))?;
-
-                index = if count_from_front {
-                    // <=> input number is positive
-                    num
-                } else {
-                    (len - 1).checked_sub(num).ok_or_else(|| {
-                        Cow::Owned(
-                            "ion: popd: negative directory stack index out of range".to_owned(),
-                        )
-                    })?
-                };
-            }
-        }
+        let index = if keep_front { 1 } else { 0 };
+        let new_dir = self.normalize_path(path.to_str().unwrap());
+        self.insert_dir(index, new_dir, variables);
+        self.set_current_dir_by_index(index)
+    }
 
-        // apply -n
-        if index == 0 && keep_front {
-            index = 1;
-        } else if index == 0 {
-            // change to new directory, return if not possible
-            self.set_current_dir_by_index(1, "popd")?;
-        }
+    /// Attempts to set the current directory to the directory stack's previous directory,
+    /// and then removes the front directory from the stack.
+    pub fn popd(&mut self, index: usize) -> Option<PathBuf> { self.dirs.remove(index) }
 
-        // pop element
-        if self.dirs.remove(index).is_some() {
-            self.print_dirs();
-            Ok(())
-        } else {
-            Err(Cow::Owned(format!("ion: popd: {}: directory stack index out of range", index)))
-        }
-    }
+    pub fn clear(&mut self) { self.dirs.truncate(1) }
 
     /// This function will take a map of variables as input and attempt to parse the value of
     /// the
@@ -393,19 +227,3 @@ impl DirectoryStack {
         DirectoryStack { dirs }
     }
 }
-
-// parses -N or +N patterns
-// required for popd, pushd, dirs
-fn parse_numeric_arg(arg: &str) -> Option<(bool, usize)> {
-    match arg.chars().nth(0) {
-        Some('+') => Some(true),
-        Some('-') => Some(false),
-        _ => None,
-    }
-    .and_then(|b| arg[1..].parse::<usize>().ok().map(|num| (b, num)))
-}
-
-// converts pbuf to an absolute path if possible
-fn try_abs_path(pbuf: &PathBuf) -> Cow<str> {
-    Cow::Owned(pbuf.canonicalize().unwrap_or_else(|_| pbuf.clone()).to_string_lossy().to_string())
-}
diff --git a/src/lib/shell/mod.rs b/src/lib/shell/mod.rs
index 21a5599ea3e1d909ce5e510372ad72b9cfedb9b1..373de69af8b212c4dcd949c22beb50e19a49e271 100644
--- a/src/lib/shell/mod.rs
+++ b/src/lib/shell/mod.rs
@@ -12,18 +12,13 @@ pub(crate) mod signals;
 pub mod status;
 pub mod variables;
 
-pub use self::{
-    fork::{Capture, IonResult},
-    job::Job,
-    pipe_exec::job_control::ProcessState,
-    variables::Value,
-};
+pub use self::{fork::Capture, job::Job, pipe_exec::job_control::ProcessState, variables::Value};
 
 use self::{
     directory_stack::DirectoryStack,
     flow_control::{Block, Function, FunctionError, Statement},
     foreground::ForegroundSignals,
-    fork::Fork,
+    fork::{Fork, IonResult},
     pipe_exec::{foreground, job_control::BackgroundProcess},
     status::*,
     variables::{GetVariable, Variables},
@@ -40,7 +35,7 @@ use std::{
     fmt,
     fs::{self, OpenOptions},
     io::{self, Write},
-    path::Path,
+    path::{Path, PathBuf},
     process,
     sync::{atomic::Ordering, Arc, Mutex},
     time::SystemTime,
@@ -205,28 +200,38 @@ impl<'a> Shell<'a> {
         }
     }
 
+    pub fn rotate_right(&mut self, num: usize) -> Result<(), Cow<'static, str>> {
+        self.directory_stack.rotate_right(num)
+    }
+
+    pub fn rotate_left(&mut self, num: usize) -> Result<(), Cow<'static, str>> {
+        self.directory_stack.rotate_left(num)
+    }
+
+    pub fn swap(&mut self, index: usize) -> Result<(), Cow<'static, str>> {
+        self.directory_stack.swap(index)
+    }
+
+    pub fn set_current_dir_by_index(&self, index: usize) -> Result<(), Cow<'static, str>> {
+        self.directory_stack.set_current_dir_by_index(index)
+    }
+
     pub fn cd<T: AsRef<str>>(&mut self, dir: Option<T>) -> Result<(), Cow<'static, str>> {
         self.directory_stack.cd(dir, &mut self.variables)
     }
 
-    pub fn pushd<T: AsRef<str>, I: IntoIterator<Item = T>>(
-        &mut self,
-        iter: I,
-    ) -> Result<(), Cow<'static, str>> {
-        self.directory_stack.pushd(iter, &mut self.variables)
+    pub fn pushd(&mut self, path: PathBuf, keep_front: bool) -> Result<(), Cow<'static, str>> {
+        self.directory_stack.pushd(path, keep_front, &mut self.variables)
     }
 
-    pub fn popd<T: AsRef<str>, I: IntoIterator<Item = T>>(
-        &mut self,
-        iter: I,
-    ) -> Result<(), Cow<'static, str>> {
-        self.directory_stack.popd(iter)
-    }
+    pub fn popd(&mut self, index: usize) -> Option<PathBuf> { self.directory_stack.popd(index) }
 
-    pub fn dir_stack<T: AsRef<str>, I: IntoIterator<Item = T>>(&mut self, iter: I) -> i32 {
-        self.directory_stack.dirs(iter)
+    pub fn dir_stack(&self) -> impl DoubleEndedIterator<Item = &PathBuf> + ExactSizeIterator {
+        self.directory_stack.dirs()
     }
 
+    pub fn clear_dir_stack(&mut self) { self.directory_stack.clear() }
+
     /// Resets the flow control fields to their default values.
     pub fn reset_flow(&mut self) { self.flow_control.clear(); }