From 38b97f39b38b89f9588d617ee5cee7a92d0f46ab Mon Sep 17 00:00:00 2001 From: Oliver Atkinson Date: Tue, 26 Nov 2024 10:40:27 -0700 Subject: [PATCH] added env and simplifed command choosing system --- src/command.rs | 232 +++++++++++++++++++++++++++---------------------- src/main.rs | 29 ++++--- 2 files changed, 147 insertions(+), 114 deletions(-) diff --git a/src/command.rs b/src/command.rs index 5b6b64e..e2fd2d9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,11 +1,50 @@ use std::{ - fs, + collections::HashMap, + fs::{self, ReadDir}, path::Path, process, }; -pub type Param<'a, 'b> = &'b [&'a str]; -trait Run = Fn(Param); +type InnerEnv = HashMap; +pub struct Env { + inner: InnerEnv, +} + +impl Env { + 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('$') { + // 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]; +trait Run = Fn(Args, &mut Env); type Cmd = Box; pub struct Command { @@ -26,132 +65,111 @@ impl Command { } } - pub fn call(&self, words: Param) { + pub fn call(&self, words: Args, env: &mut Env) { + // apply env + let words: Args = &env.apply_args_substitution(words); + let x = &self.cmd; - x(words) + x(words, env) } - pub fn choose<'a>(text: &str, choices: &'a Vec) -> Option<&'a Self> { - // TODO there seems to be duplicates in here? - + pub fn choose<'a>(text: &str, choices: &'a [Self]) -> Option<&'a Self> { // See what command the user is trying to call - // iter thru all the commands - let mut ranked = choices.iter().fold(Vec::new(), |mut v, cmd| { - // count how many chars match from the command to the word - let matches = text - .chars() - .zip(cmd.name.chars()) - .fold(0, |mut f, (word, cmd)| { - if word == cmd { - f += 1; - } - f - }); - v.push((cmd, matches)); - v - }); + let matches = choices + .iter() + .filter(|cmd| cmd.name.starts_with(text)) + .collect::>(); - // sort by most matches - ranked.sort_by(|a, b| b.1.cmp(&a.1)); + // 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() }; - // Don't select commands just because they are first. The command also has to have a - // confidence of at least 1 - if let Some(first) = ranked.first() { - if first.1 < 1 { - return None; + 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; } - // TODO test when 1 100% match exists - - if ranked.len() > 1 { - // If the top two command choices are ranked the same, we don't know which to choose! - let top = ranked[0]; - let second = ranked[1]; - if top.1 == second.1 { - println!("Ambigous command '{text}'. Could be any of: "); - let iter: Vec<&(&Command, usize)> = ranked - .iter() - // We only care about 100% matches - .filter(|f| f.1 >= text.len()) - .collect(); - - let max_display = if 5 < iter.len() {5} else {iter.len()}; - - iter[..max_display].iter().for_each(|matching| { - println!("\t- {}", matching.0.name); - }); - - if iter.len() > max_display { - println!("\t+ Omitting {} other entries...", iter.len() - max_display); - } - - return None; - } - } - - if let Some((cmd, _)) = ranked.first() { + if let Some(cmd) = matches.first() { return Some(cmd); } None } - pub fn all_commands() -> Vec { - let path = Path::new("/bin"); - let res = fs::read_dir(path).expect("Failed to read /bin"); - - // load all the programs from /bin - 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: Param| { - let name = cmd.path().to_str().unwrap().to_owned(); - let out = process::Command::new(name) - .args(&args[1..]) - .output() - .expect("Failed to call external program"); - // TODO load into env - let status = out.status.code(); - 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}"); - } - }), - ); - v.push(cmd); - } + fn load_from(path: &str) -> Vec { + let path = Path::new(path); + if let Ok(res) = fs::read_dir(path) { + // load all the programs from /bin + 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 Env| { + 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"); + // TODO load into env + if let Some(status) = out.status.code() { + env.set("?", &status.to_string()); + } + 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}"); + } + }), + ); + v.push(cmd); + } + }; }; - }; - v - }); + v + }); + return bin; + } + // invalid path was passed, just return an empty vec + Vec::new() + } + pub fn all_commands(env: &Env) -> Vec { // load custom programs let mut commands = vec![ Command::init( "exit", vec![], - Box::new(|_| { + Box::new(|_, _| { std::process::exit(0); }), ), Command::init( - "custom_echo", + "echo", vec![], - Box::new(|args| { + Box::new(|args, _| { let msg = &args[1..] .iter() .map(|s| s.to_owned()) - .intersperse(" ") + .intersperse(" ".to_string()) .collect::(); println!("{msg}"); }), @@ -162,7 +180,7 @@ impl Command { Command::init( "number", vec![], - Box::new(|_| { + Box::new(|_, _| { // how do you know it's not random? println!("8"); }), @@ -170,20 +188,26 @@ impl Command { Command::init( "string", vec![], - Box::new(|_| { + Box::new(|_, _| { // how do you know it's not random? println!("hello world"); }), ), ], - Box::new(|args| { + Box::new(|args, _| { println!("random's debug: {:?}", args); }), ), ]; + // $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.extend(bin); commands } } diff --git a/src/main.rs b/src/main.rs index f80fc75..fed5a2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![feature(iter_intersperse)] #![feature(trait_alias)] use std::io::{BufRead, Write}; -use command::{Command, Param}; +use command::{Command, Env, Args}; mod command; @@ -11,14 +11,19 @@ fn main() { let stdout = std::io::stdout(); let mut out_lock = stdout.lock(); + + let mut env= Env::new(); + env.set("HOME", "/a/fake/dir"); + env.set("PS1", "> "); - let commands = &Command::all_commands(); + let commands = &Command::all_commands(&env); println!("Loaded {} commands", commands.len()); loop { let mut buf = String::new(); - let ps1 = "> ".as_bytes(); + let default_ps1 = "> ".to_string(); + let ps1 = env.get("PS1").unwrap_or(&default_ps1).as_bytes(); out_lock.write(ps1).unwrap(); out_lock.flush().unwrap(); @@ -26,18 +31,20 @@ fn main() { .read_line(&mut buf) .expect("Failed to read in to buffer."); - let mut args: Param = &buf.trim().split(' ').collect::>(); + let mut args: Args = &buf.trim().split(' ').map(|f| f.to_owned()).collect::>(); + + // let mut args: Args = &buf.trim().split(' ').collect::>(); let mut options = commands; loop { - match args.get(0) { + match args.first() { Some(cmd_arg) => { // Try to find a subcommand to run match Command::choose(cmd_arg, options) { Some(choice) => { // No sub commands left to choose from, just pass the input if choice.sub_commands.is_empty() { - choice.call(&args); + choice.call(args, &mut env); break; } @@ -46,14 +53,17 @@ fn main() { // a sort of last chance before running into hella edgecases if args.is_empty() - || Command::choose(args[0], options).is_none() // yes, this means that we search for a choice twice if it misses, but the search takes all of 0ms, so it's fine. + || Command::choose(&args[0], options).is_none() // yes, this means that we search for a choice twice if it misses, but the search takes all of 0ms, so it's fine. { - choice.call(args); + choice.call(args, &mut env); break; } } // When no good choice presents it's self - None => break, + None => { + println!("No option..."); + break; + }, } } None => unimplemented!("Couldn't get index 0 of the args"), @@ -61,4 +71,3 @@ fn main() { } } } -