added env and simplifed command choosing system

This commit is contained in:
Oliver Atkinson 2024-11-26 10:40:27 -07:00
parent c0fff34a7c
commit 38b97f39b3
2 changed files with 147 additions and 114 deletions

View File

@ -1,11 +1,50 @@
use std::{ use std::{
fs, collections::HashMap,
fs::{self, ReadDir},
path::Path, path::Path,
process, process,
}; };
pub type Param<'a, 'b> = &'b [&'a str]; type InnerEnv = HashMap<String, String>;
trait Run = Fn(Param); 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<String> {
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<dyn Run>; type Cmd = Box<dyn Run>;
pub struct Command { pub struct Command {
@ -26,80 +65,51 @@ 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; let x = &self.cmd;
x(words) x(words, env)
} }
pub fn choose<'a>(text: &str, choices: &'a Vec<Self>) -> Option<&'a Self> { pub fn choose<'a>(text: &str, choices: &'a [Self]) -> Option<&'a Self> {
// TODO there seems to be duplicates in here?
// See what command the user is trying to call // See what command the user is trying to call
// iter thru all the commands let matches = choices
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
});
// sort by most matches
ranked.sort_by(|a, b| b.1.cmp(&a.1));
// 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;
}
}
// 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() .iter()
// We only care about 100% matches .filter(|cmd| cmd.name.starts_with(text))
.filter(|f| f.1 >= text.len()) .collect::<Vec<&Command>>();
.collect();
let max_display = if 5 < iter.len() {5} else {iter.len()}; // 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() };
iter[..max_display].iter().for_each(|matching| { matches[..max_display].iter().for_each(|matching| {
println!("\t- {}", matching.0.name); println!("\t- {}", matching.name);
}); });
if iter.len() > max_display { if matches.len() > max_display {
println!("\t+ Omitting {} other entries...", iter.len() - max_display); println!(
"\t+ Omitting {} other entries...",
matches.len() - max_display
);
} }
return None; return None;
} }
}
if let Some((cmd, _)) = ranked.first() { if let Some(cmd) = matches.first() {
return Some(cmd); return Some(cmd);
} }
None None
} }
pub fn all_commands() -> Vec<Self> { fn load_from(path: &str) -> Vec<Command> {
let path = Path::new("/bin"); let path = Path::new(path);
let res = fs::read_dir(path).expect("Failed to read /bin"); if let Ok(res) = fs::read_dir(path) {
// load all the programs from /bin // load all the programs from /bin
let bin = res.fold(Vec::new(), |mut v, contents| { let bin = res.fold(Vec::new(), |mut v, contents| {
if let Ok(cmd) = contents { if let Ok(cmd) = contents {
@ -109,16 +119,18 @@ impl Command {
let cmd = Command::init( let cmd = Command::init(
cmd.file_name().into_string().unwrap(), cmd.file_name().into_string().unwrap(),
vec![], vec![],
Box::new(move |args: Param| { Box::new(move |args: Args, env: &mut Env| {
let name = cmd.path().to_str().unwrap().to_owned(); let name = cmd.path().to_str().unwrap().to_owned();
let out = process::Command::new(name) let out = process::Command::new(name)
.args(&args[1..]) .args(&args[1..])
.envs(env.as_env())
.output() .output()
.expect("Failed to call external program"); .expect("Failed to call external program");
// TODO load into env // TODO load into env
let status = out.status.code(); if let Some(status) = out.status.code() {
if !out.stdout.is_empty() env.set("?", &status.to_string());
{ }
if !out.stdout.is_empty() {
let output = String::from_utf8_lossy(&out.stdout); let output = String::from_utf8_lossy(&out.stdout);
println!("{output}"); println!("{output}");
} }
@ -134,24 +146,30 @@ impl Command {
}; };
v v
}); });
return bin;
}
// invalid path was passed, just return an empty vec
Vec::new()
}
pub fn all_commands(env: &Env) -> Vec<Self> {
// load custom programs // load custom programs
let mut commands = vec![ let mut commands = vec![
Command::init( Command::init(
"exit", "exit",
vec![], vec![],
Box::new(|_| { Box::new(|_, _| {
std::process::exit(0); std::process::exit(0);
}), }),
), ),
Command::init( Command::init(
"custom_echo", "echo",
vec![], vec![],
Box::new(|args| { Box::new(|args, _| {
let msg = &args[1..] let msg = &args[1..]
.iter() .iter()
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.intersperse(" ") .intersperse(" ".to_string())
.collect::<String>(); .collect::<String>();
println!("{msg}"); println!("{msg}");
}), }),
@ -162,7 +180,7 @@ impl Command {
Command::init( Command::init(
"number", "number",
vec![], vec![],
Box::new(|_| { Box::new(|_, _| {
// how do you know it's not random? // how do you know it's not random?
println!("8"); println!("8");
}), }),
@ -170,20 +188,26 @@ impl Command {
Command::init( Command::init(
"string", "string",
vec![], vec![],
Box::new(|_| { Box::new(|_, _| {
// how do you know it's not random? // how do you know it's not random?
println!("hello world"); println!("hello world");
}), }),
), ),
], ],
Box::new(|args| { Box::new(|args, _| {
println!("random's debug: {:?}", args); println!("random's debug: {:?}", args);
}), }),
), ),
]; ];
// $PATH
if let Some(path_var) = env.get("PATH") {
let paths = path_var.split(';').collect::<Vec<&str>>();
for path in paths {
commands.extend(Command::load_from(path));
}
}
// combine with a move // combine with a move
commands.extend(bin);
commands commands
} }
} }

View File

@ -1,7 +1,7 @@
#![feature(iter_intersperse)] #![feature(iter_intersperse)]
#![feature(trait_alias)] #![feature(trait_alias)]
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use command::{Command, Param}; use command::{Command, Env, Args};
mod command; mod command;
@ -12,13 +12,18 @@ fn main() {
let stdout = std::io::stdout(); let stdout = std::io::stdout();
let mut out_lock = stdout.lock(); let mut out_lock = stdout.lock();
let commands = &Command::all_commands(); let mut env= Env::new();
env.set("HOME", "/a/fake/dir");
env.set("PS1", "> ");
let commands = &Command::all_commands(&env);
println!("Loaded {} commands", commands.len()); println!("Loaded {} commands", commands.len());
loop { loop {
let mut buf = String::new(); 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.write(ps1).unwrap();
out_lock.flush().unwrap(); out_lock.flush().unwrap();
@ -26,18 +31,20 @@ fn main() {
.read_line(&mut buf) .read_line(&mut buf)
.expect("Failed to read in to buffer."); .expect("Failed to read in to buffer.");
let mut args: Param = &buf.trim().split(' ').collect::<Vec<&str>>(); let mut args: Args = &buf.trim().split(' ').map(|f| f.to_owned()).collect::<Vec<String>>();
// let mut args: Args = &buf.trim().split(' ').collect::<Vec<&str>>();
let mut options = commands; let mut options = commands;
loop { loop {
match args.get(0) { match args.first() {
Some(cmd_arg) => { Some(cmd_arg) => {
// Try to find a subcommand to run // Try to find a subcommand to run
match Command::choose(cmd_arg, options) { match Command::choose(cmd_arg, options) {
Some(choice) => { Some(choice) => {
// No sub commands left to choose from, just pass the input // No sub commands left to choose from, just pass the input
if choice.sub_commands.is_empty() { if choice.sub_commands.is_empty() {
choice.call(&args); choice.call(args, &mut env);
break; break;
} }
@ -46,14 +53,17 @@ fn main() {
// a sort of last chance before running into hella edgecases // a sort of last chance before running into hella edgecases
if args.is_empty() 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; break;
} }
} }
// When no good choice presents it's self // 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"), None => unimplemented!("Couldn't get index 0 of the args"),
@ -61,4 +71,3 @@ fn main() {
} }
} }
} }