pub(crate) mod alias;
pub(crate) mod cd;
mod cwd;
pub(crate) mod dirstack;
mod exit;
mod export;
mod help;
mod history;
mod restart;
pub(crate) mod source;
pub(crate) mod unalias;
mod unset;
pub(crate) mod update;
pub(crate) mod which_type;
use super::CommandResult;
fn parse_args<T: clap::Parser>(cmd: &str, args: &[&str]) -> Result<T, CommandResult> {
T::try_parse_from(std::iter::once(cmd).chain(args.iter().copied())).map_err(|e| {
let msg = e.to_string();
if e.use_stderr() {
eprint!("{msg}");
CommandResult::error(msg, 2)
} else {
print!("{msg}");
CommandResult::success(msg)
}
})
}
pub fn is_builtin(cmd: &str) -> bool {
matches!(
cmd,
"alias"
| "cd"
| "cwd"
| "dirs"
| "pwd"
| "exit"
| "export"
| "help"
| "popd"
| "pushd"
| "restart"
| "source"
| "unalias"
| "unset"
| "update"
| "history"
| "which"
| "type"
)
}
pub fn dispatch_builtin(cmd: &str, args: &[&str]) -> Option<CommandResult> {
match cmd {
"alias" => Some(alias::execute_with_aliases(
args,
&mut std::collections::HashMap::new(),
)),
"cd" => Some(cd::execute(args, &mut Vec::new())),
"cwd" | "pwd" => Some(cwd::execute(args)),
"dirs" => Some(dirstack::execute_dirs(args, &mut Vec::new())),
"exit" => Some(exit::execute(args)),
"export" => Some(export::execute(args)),
"help" => Some(help::execute(args)),
"unalias" => Some(unalias::execute_with_aliases(
args,
&mut std::collections::HashMap::new(),
)),
"source" => {
Some(source::parse(args).map_or_else(|e| e, |_| CommandResult::success(String::new())))
}
"pushd" => Some(dirstack::execute_pushd(args, &mut Vec::new())),
"popd" => Some(dirstack::execute_popd(args, &mut Vec::new())),
"unset" => Some(unset::execute(args)),
"history" => Some(history::execute(args)),
"restart" => Some(restart::execute(args)),
"update" => Some(update::execute(args)),
"which" => Some(which_type::execute_which(
args,
&std::collections::HashMap::new(),
)),
"type" => Some(which_type::execute_type(
args,
&std::collections::HashMap::new(),
)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::cwd::test_helpers::CwdGuard;
use super::*;
use serial_test::serial;
use std::env;
use std::path::PathBuf;
#[test]
fn unknown_command_returns_none() {
assert!(dispatch_builtin("ls", &[]).is_none());
assert!(dispatch_builtin("git", &["status"]).is_none());
}
#[test]
#[serial]
fn cwd_reflects_cd_change() {
let _guard = CwdGuard::new();
let tmpdir = tempfile::tempdir().expect("failed to create tempdir");
let target = tmpdir.path().to_path_buf();
let cd_result = dispatch_builtin("cd", &[target.to_str().unwrap()]).unwrap();
assert_eq!(cd_result.exit_code, 0);
let cwd_result = dispatch_builtin("cwd", &[]).unwrap();
assert_eq!(cwd_result.exit_code, 0);
assert_eq!(
PathBuf::from(cwd_result.stdout.trim())
.canonicalize()
.unwrap(),
target.canonicalize().unwrap()
);
}
#[test]
#[serial]
fn cwd_unchanged_after_cd_failure() {
let _guard = CwdGuard::new();
let before = env::current_dir().unwrap();
let cd_result = dispatch_builtin("cd", &["/nonexistent_path_that_does_not_exist"]).unwrap();
assert_ne!(cd_result.exit_code, 0);
let cwd_result = dispatch_builtin("cwd", &[]).unwrap();
assert_eq!(cwd_result.exit_code, 0);
assert_eq!(
PathBuf::from(cwd_result.stdout.trim())
.canonicalize()
.unwrap(),
before.canonicalize().unwrap()
);
}
#[test]
#[serial]
fn cd_sequential_moves_tracked_by_cwd() {
let _guard = CwdGuard::new();
let dir1 = tempfile::tempdir().expect("failed to create tempdir");
let dir2 = tempfile::tempdir().expect("failed to create tempdir");
dispatch_builtin("cd", &[dir1.path().to_str().unwrap()]).unwrap();
let cwd1 = dispatch_builtin("cwd", &[]).unwrap();
assert_eq!(
PathBuf::from(cwd1.stdout.trim()).canonicalize().unwrap(),
dir1.path().canonicalize().unwrap()
);
dispatch_builtin("cd", &[dir2.path().to_str().unwrap()]).unwrap();
let cwd2 = dispatch_builtin("cwd", &[]).unwrap();
assert_eq!(
PathBuf::from(cwd2.stdout.trim()).canonicalize().unwrap(),
dir2.path().canonicalize().unwrap()
);
}
#[test]
#[serial]
fn pwd_is_alias_for_cwd() {
let _guard = CwdGuard::new();
assert!(is_builtin("pwd"));
let pwd_result = dispatch_builtin("pwd", &[]).unwrap();
assert_eq!(pwd_result.exit_code, 0);
let cwd_result = dispatch_builtin("cwd", &[]).unwrap();
assert_eq!(pwd_result.stdout, cwd_result.stdout);
}
#[test]
fn new_builtins_are_registered() {
assert!(is_builtin("alias"));
assert!(is_builtin("dirs"));
assert!(is_builtin("export"));
assert!(is_builtin("help"));
assert!(is_builtin("popd"));
assert!(is_builtin("pushd"));
assert!(is_builtin("source"));
assert!(is_builtin("unalias"));
assert!(is_builtin("unset"));
assert!(is_builtin("history"));
}
#[test]
fn new_builtins_dispatch_returns_some() {
assert!(dispatch_builtin("export", &[]).is_some());
assert!(dispatch_builtin("history", &["--help"]).is_some());
}
}