Compare commits
	
		
			4 Commits
		
	
	
		
			51c7528556
			...
			e2df94a6c2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e2df94a6c2 | ||
| 
						 | 
					38b97f39b3 | ||
| 
						 | 
					c0fff34a7c | ||
| 
						 | 
					00b4583f3b | 
							
								
								
									
										20
									
								
								bash_info.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								bash_info.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# invalid
 | 
			
		||||
```bash
 | 
			
		||||
$a="b"
 | 
			
		||||
a = "b"
 | 
			
		||||
a= "b"
 | 
			
		||||
a ="b"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# valid
 | 
			
		||||
```bash
 | 
			
		||||
a="b"
 | 
			
		||||
a=b
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# examples
 | 
			
		||||
```bash
 | 
			
		||||
a="b c d"
 | 
			
		||||
echo $a
 | 
			
		||||
# output: b c d
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										226
									
								
								src/command.rs
									
									
									
									
									
								
							
							
						
						
									
										226
									
								
								src/command.rs
									
									
									
									
									
								
							@@ -1,11 +1,52 @@
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    fs,
 | 
			
		||||
    path::Path,
 | 
			
		||||
    process,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub type Param<'a, 'b> = &'b [&'a str];
 | 
			
		||||
trait Run = Fn(Param);
 | 
			
		||||
use crate::SPECIAL_CHAR;
 | 
			
		||||
 | 
			
		||||
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(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];
 | 
			
		||||
trait Run = Fn(Args, &mut Env);
 | 
			
		||||
type Cmd = Box<dyn Run>;
 | 
			
		||||
 | 
			
		||||
pub struct Command {
 | 
			
		||||
@@ -26,129 +67,108 @@ impl Command {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn call(&self, words: Param) {
 | 
			
		||||
    pub fn call(&self, words: Args, env: &mut Env) {
 | 
			
		||||
        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| {
 | 
			
		||||
                                let name = cmd.path().to_str().unwrap().to_owned();
 | 
			
		||||
                                let out = process::Command::new(name)
 | 
			
		||||
                                    .args(args)
 | 
			
		||||
                                    .output()
 | 
			
		||||
                                    .expect("Failed to call external program");
 | 
			
		||||
                                // FIXME not printing to out
 | 
			
		||||
                                //
 | 
			
		||||
                                let output = String::from_utf8_lossy(&out.stdout);
 | 
			
		||||
                                let errout = String::from_utf8_lossy(&out.stderr);
 | 
			
		||||
                                // TODO load into env
 | 
			
		||||
                                let status = out.status.code();
 | 
			
		||||
                                println!("{:?} output: {output}", status);
 | 
			
		||||
                                println!("{:?} err: {errout}", status);
 | 
			
		||||
                            }),
 | 
			
		||||
                        );
 | 
			
		||||
                        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}");
 | 
			
		||||
                }),
 | 
			
		||||
@@ -159,7 +179,7 @@ impl Command {
 | 
			
		||||
                    Command::init(
 | 
			
		||||
                        "number",
 | 
			
		||||
                        vec![],
 | 
			
		||||
                        Box::new(|_| {
 | 
			
		||||
                        Box::new(|_, _| {
 | 
			
		||||
                            // how do you know it's not random?
 | 
			
		||||
                            println!("8");
 | 
			
		||||
                        }),
 | 
			
		||||
@@ -167,20 +187,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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -1,24 +1,30 @@
 | 
			
		||||
#![feature(iter_intersperse)]
 | 
			
		||||
#![feature(trait_alias)]
 | 
			
		||||
use std::io::{BufRead, Write};
 | 
			
		||||
use command::{Command, Param};
 | 
			
		||||
use command::{Command, Env, Args};
 | 
			
		||||
 | 
			
		||||
mod command;
 | 
			
		||||
 | 
			
		||||
pub static SPECIAL_CHAR: char = '$';
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    let stdin = std::io::stdin();
 | 
			
		||||
    let mut in_lock = stdin.lock();
 | 
			
		||||
 | 
			
		||||
    let stdout = std::io::stdout();
 | 
			
		||||
    let mut out_lock = stdout.lock();
 | 
			
		||||
    
 | 
			
		||||
    let mut env= Env::new();
 | 
			
		||||
    env.set("HOME", "/a/fake/dir");
 | 
			
		||||
 | 
			
		||||
    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,38 +32,74 @@ fn main() {
 | 
			
		||||
            .read_line(&mut buf)
 | 
			
		||||
            .expect("Failed to read in to buffer.");
 | 
			
		||||
 | 
			
		||||
        let mut args: Param = &buf.trim().split(' ').collect::<Vec<&str>>();
 | 
			
		||||
        // TODO split at ' ' but don't split when it's surrounded by quotes '"'
 | 
			
		||||
        let args= &buf.trim().split(' ').map(|f| f.to_owned()).collect::<Vec<String>>();
 | 
			
		||||
        let args = env.apply_args_substitution(args);
 | 
			
		||||
        let mut args: Args = &args;
 | 
			
		||||
 | 
			
		||||
        // 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() {
 | 
			
		||||
                                // No sub commands left to choose from, just pass the input
 | 
			
		||||
                                choice.call(&args);
 | 
			
		||||
                                choice.call(&args, &mut env);
 | 
			
		||||
                                break;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                args = &args[1..];
 | 
			
		||||
                                options = &choice.sub_commands;
 | 
			
		||||
                            } 
 | 
			
		||||
 | 
			
		||||
                                // a sort of last chance before running into hella edgecases
 | 
			
		||||
                                if args.is_empty() {
 | 
			
		||||
                                    choice.call(args);
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            args = &args[1..];
 | 
			
		||||
                            options = &choice.sub_commands;
 | 
			
		||||
 | 
			
		||||
                            // 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.
 | 
			
		||||
                            {
 | 
			
		||||
                                choice.call(&args, &mut env);
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        // When no good choice presents it's self
 | 
			
		||||
                        None => break,
 | 
			
		||||
                        None => {
 | 
			
		||||
                            // perhaps they are trying to use builtins?
 | 
			
		||||
                            parse_lang(&args, &mut env);
 | 
			
		||||
                            break;
 | 
			
		||||
                        },
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                None => todo!("Couldn't get index 0 of the args"),
 | 
			
		||||
                None => unimplemented!("Couldn't get index 0 of the args"),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn parse_lang(tokens: Args, env: &mut Env) {
 | 
			
		||||
    if let Some(first) =  tokens.first() {
 | 
			
		||||
        if let Some((var, val)) = first.split_once('=') {
 | 
			
		||||
            let var = var.replace(SPECIAL_CHAR, "");
 | 
			
		||||
 | 
			
		||||
            // Make it so that strings don't get spliced up
 | 
			
		||||
            let val = if val.starts_with('"') {
 | 
			
		||||
                let mut buf = String::new();
 | 
			
		||||
                buf += &val;
 | 
			
		||||
                for i in &tokens[1..] {
 | 
			
		||||
                    buf += " ";
 | 
			
		||||
                    buf += i;
 | 
			
		||||
                    if i.ends_with('"') {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                buf.replace('"', "")
 | 
			
		||||
            } else {
 | 
			
		||||
                val.to_owned()
 | 
			
		||||
            };
 | 
			
		||||
            env.set(&var, &val);
 | 
			
		||||
        } else {
 | 
			
		||||
            println!("first token is not a command but also doesn't include '='");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user