188 lines
5.8 KiB
Rust
188 lines
5.8 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
fs,
|
|
path::Path,
|
|
process,
|
|
};
|
|
|
|
use crate::SPECIAL_CHAR;
|
|
|
|
type InnerEnv = HashMap<String, String>;
|
|
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<String> {
|
|
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<dyn Run>;
|
|
|
|
pub struct Command {
|
|
name: String,
|
|
pub sub_commands: Vec<Command>,
|
|
cmd: Cmd,
|
|
}
|
|
|
|
impl Command {
|
|
pub fn init<T>(name: T, sub_commands: Vec<Self>, 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::<Vec<&Command>>();
|
|
|
|
// 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<Command> {
|
|
let path = Path::new(path);
|
|
if let Ok(res) = fs::read_dir(path) {
|
|
// load all the programs from <path>
|
|
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<Self> {
|
|
// 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::<Vec<&str>>();
|
|
for path in paths {
|
|
commands.extend(Command::load_from(path));
|
|
}
|
|
}
|
|
// combine with a move
|
|
commands
|
|
}
|
|
}
|