use std::{ collections::HashMap, fs, path::Path, process, }; use crate::SPECIAL_CHAR; type InnerEnv = HashMap; pub struct CmdEnv { inner: InnerEnv, } impl std::fmt::Debug for CmdEnv { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut stu = f.debug_struct("CmdEnv"); for (k,v) in self.inner.iter() { stu.field(k, v); } stu .finish() } } impl CmdEnv { pub fn new() -> Self { Self { inner: HashMap::new(), } } pub fn as_env(&self) -> &InnerEnv { &self.inner } pub fn get(&self, var: &str) -> Option<&String> { self.inner.get(var) } pub fn set(&mut self, var: &str, val: &str) { self.inner.insert(var.to_owned(), val.to_owned()); } pub fn unset(&mut self, var: &str) { self.inner.remove(var); } pub fn apply_args_substitution(&self, args: Args) -> Vec { args.iter().fold(Vec::new(), |mut vec, arg| { if arg.starts_with(SPECIAL_CHAR) { // we take a slice to get rid of the '$' if let Some(sub) = self.get(&arg[1..]) { vec.push(sub.to_owned()); return vec; } } vec.push(arg.to_string()); vec }) } } pub type Args<'a, 'b> = &'b [String]; // A function that takes args, the env, and returns a status code trait Run = Fn(Args, &mut CmdEnv) -> i32; type Cmd = Box; pub struct Command { name: String, pub sub_commands: Vec, cmd: Cmd, } impl Command { pub fn init(name: T, sub_commands: Vec, f: Cmd) -> Self where T: ToString, { Self { name: name.to_string(), sub_commands, cmd: f, } } pub fn call(&self, words: Args, env: &mut CmdEnv) { let x = &self.cmd; let exit_code = x(words, env); env.set("?", &exit_code.to_string()); } pub fn choose<'a>(text: &str, choices: &'a [Self]) -> Option<&'a Self> { // See what command the user is trying to call let matches = choices .iter() .filter(|cmd| cmd.name.starts_with(text)) .collect::>(); // If there are more than two commands that match the filter we don't know which one they want if matches.len() > 1 { // ❗ Here till the return is all just debug output println!("Ambiguous command '{text}'. Could be any of: "); // The most amount of choices that will be presented as collisions let max_display = 5; let max_display = if max_display < matches.len() { max_display } else { matches.len() }; matches[..max_display].iter().for_each(|matching| { println!("\t- {}", matching.name); }); if matches.len() > max_display { println!( "\t+ Omitting {} other entries...", matches.len() - max_display ); } return None; } if let Some(cmd) = matches.first() { return Some(cmd); } None } fn load_from(path: &str) -> Vec { let path = Path::new(path); if let Ok(res) = fs::read_dir(path) { // load all the programs from let bin = res.fold(Vec::new(), |mut v, contents| { if let Ok(cmd) = contents { if let Ok(t) = cmd.file_type() { if t.is_file() { // v.push(cmd.path()); let cmd = Command::init( cmd.file_name().into_string().unwrap(), vec![], Box::new(move |args: Args, env: &mut CmdEnv| { let name = cmd.path().to_str().unwrap().to_owned(); let out = process::Command::new(name) .args(&args[1..]) .envs(env.as_env()) .output() .expect("Failed to call external program"); if !out.stdout.is_empty() { let output = String::from_utf8_lossy(&out.stdout); println!("{output}"); } if !out.stderr.is_empty() { let errout = String::from_utf8_lossy(&out.stderr); println!("{errout}"); } out.status.code().unwrap_or(0) }), ); v.push(cmd); } }; }; v }); return bin; } // invalid path was passed, just return an empty vec Vec::new() } pub fn all_commands(env: &CmdEnv) -> Vec { // load custom programs let mut commands = vec![ Command::init( "exit", vec![], Box::new(|_, _| { std::process::exit(0); }), ), crate::cmd::echo::init(), crate::cmd::random::init(), ]; // $PATH if let Some(path_var) = env.get("PATH") { let paths = path_var.split(';').collect::>(); for path in paths { commands.extend(Command::load_from(path)); } } // combine with a move commands } }