use std::path::PathBuf;
use crate::env::ShellEnv;
use crate::error::ShellError;
use crate::exec::command::{find_in_path, is_executable_file};
pub fn builtin_hash(args: &[String], env: &mut ShellEnv) -> Result<i32, ShellError> {
let mut clear = false;
let mut idx = 0;
while idx < args.len() {
let a = &args[idx];
if a == "--" {
idx += 1;
break;
}
if !a.starts_with('-') || a == "-" {
break;
}
for ch in a[1..].chars() {
match ch {
'r' => clear = true,
other => {
eprintln!("yosh: hash: -{}: invalid option", other);
return Ok(1);
}
}
}
idx += 1;
}
let operands = &args[idx..];
if clear {
env.utility_hash.clear();
}
if operands.is_empty() {
if clear {
return Ok(0);
}
let mut names: Vec<&String> = env.utility_hash.keys().collect();
names.sort();
for name in names {
if let Some(path) = env.utility_hash.get(name) {
println!("{}", path.display());
}
}
return Ok(0);
}
let mut exit_status = 0;
for name in operands {
if name.contains('/') {
let path = PathBuf::from(name);
if !is_executable_file(&path) {
eprintln!("yosh: hash: {}: not found", name);
exit_status = 1;
continue;
}
let basename = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| name.clone());
env.utility_hash.insert(basename, path);
} else {
let path_var = env
.vars
.get("PATH")
.map(|s| s.to_string())
.unwrap_or_default();
match find_in_path(name, &path_var, &mut env.utility_hash) {
Some(_) => {
}
None => {
eprintln!("yosh: hash: {}: not found", name);
exit_status = 1;
}
}
}
}
Ok(exit_status)
}
#[cfg(test)]
mod tests {
use super::*;
fn env_with_path(path: &str) -> ShellEnv {
let mut env = ShellEnv::new("yosh", vec![]);
let _ = env.vars.set("PATH", path);
env
}
#[test]
fn r_flag_clears_cache() {
let mut env = env_with_path("/bin:/usr/bin");
env.utility_hash
.insert("foo".to_string(), PathBuf::from("/bin/foo"));
let args = vec!["-r".to_string()];
let r = builtin_hash(&args, &mut env).unwrap();
assert_eq!(r, 0);
assert!(env.utility_hash.is_empty());
}
#[test]
fn slash_path_to_nonexistent_returns_error() {
let mut env = env_with_path("/bin:/usr/bin");
let args = vec!["/no/such/cmd_definitely_missing_12345".to_string()];
let r = builtin_hash(&args, &mut env).unwrap();
assert_eq!(r, 1);
assert!(env.utility_hash.is_empty());
}
#[test]
fn name_lookup_succeeds_for_sh() {
let path_var = std::env::var("PATH").unwrap_or_else(|_| "/bin:/usr/bin".to_string());
let mut env = env_with_path(&path_var);
let args = vec!["sh".to_string()];
let r = builtin_hash(&args, &mut env).unwrap();
assert_eq!(r, 0);
assert!(env.utility_hash.contains_key("sh"));
}
#[test]
fn no_args_empty_cache_returns_zero() {
let mut env = env_with_path("/bin:/usr/bin");
let r = builtin_hash(&[], &mut env).unwrap();
assert_eq!(r, 0);
}
#[test]
fn invalid_option_returns_one() {
let mut env = env_with_path("/bin:/usr/bin");
let args = vec!["-x".to_string()];
let r = builtin_hash(&args, &mut env).unwrap();
assert_eq!(r, 1);
}
#[test]
fn nonexistent_name_returns_one() {
let mut env = env_with_path("/bin:/usr/bin");
let args = vec!["definitely_no_such_cmd_98765".to_string()];
let r = builtin_hash(&args, &mut env).unwrap();
assert_eq!(r, 1);
}
#[test]
fn r_with_operand_clears_then_lookups() {
let path_var = std::env::var("PATH").unwrap_or_else(|_| "/bin:/usr/bin".to_string());
let mut env = env_with_path(&path_var);
env.utility_hash
.insert("stale".to_string(), PathBuf::from("/old/stale"));
let args = vec!["-r".to_string(), "sh".to_string()];
let r = builtin_hash(&args, &mut env).unwrap();
assert_eq!(r, 0);
assert!(!env.utility_hash.contains_key("stale"));
assert!(env.utility_hash.contains_key("sh"));
}
}