added env and simplifed command choosing system
This commit is contained in:
parent
c0fff34a7c
commit
38b97f39b3
232
src/command.rs
232
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<String, String>;
|
||||
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>;
|
||||
|
||||
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<Self>) -> 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::<Vec<&Command>>();
|
||||
|
||||
// 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<Self> {
|
||||
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<Command> {
|
||||
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<Self> {
|
||||
// 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::<String>();
|
||||
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::<Vec<&str>>();
|
||||
for path in paths {
|
||||
commands.extend(Command::load_from(path));
|
||||
}
|
||||
}
|
||||
// combine with a move
|
||||
commands.extend(bin);
|
||||
commands
|
||||
}
|
||||
}
|
||||
|
29
src/main.rs
29
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;
|
||||
|
||||
@ -12,13 +12,18 @@ fn main() {
|
||||
let stdout = std::io::stdout();
|
||||
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());
|
||||
|
||||
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::<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;
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user