Compare commits
12 Commits
50173b1ed5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6de062e808 | |||
|
|
9cc1a64a91 | ||
|
|
e2df94a6c2 | ||
|
|
38b97f39b3 | ||
|
|
c0fff34a7c | ||
|
|
00b4583f3b | ||
| 51c7528556 | |||
| ea48cda85d | |||
| bc844cb5fa | |||
| c71ea179ec | |||
|
|
9af20c0424 | ||
|
|
e57e7ff907 |
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
|
||||||
|
```
|
||||||
17
src/cmd/echo.rs
Normal file
17
src/cmd/echo.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::command::Command;
|
||||||
|
|
||||||
|
pub fn init() -> Command {
|
||||||
|
Command::init(
|
||||||
|
"echo",
|
||||||
|
vec![],
|
||||||
|
Box::new(|args, _| {
|
||||||
|
let msg = &args[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.intersperse(" ".to_string())
|
||||||
|
.collect::<String>();
|
||||||
|
println!("{msg}");
|
||||||
|
0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
2
src/cmd/mod.rs
Normal file
2
src/cmd/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod echo;
|
||||||
|
pub mod random;
|
||||||
31
src/cmd/random.rs
Normal file
31
src/cmd/random.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use crate::command::Command;
|
||||||
|
|
||||||
|
pub fn init() -> Command {
|
||||||
|
Command::init(
|
||||||
|
"random",
|
||||||
|
vec![
|
||||||
|
Command::init(
|
||||||
|
"number",
|
||||||
|
vec![],
|
||||||
|
Box::new(|_, _| {
|
||||||
|
// how do you know it's not random?
|
||||||
|
println!("8");
|
||||||
|
0
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Command::init(
|
||||||
|
"string",
|
||||||
|
vec![],
|
||||||
|
Box::new(|_, _| {
|
||||||
|
// how do you know it's not random?
|
||||||
|
println!("hello world");
|
||||||
|
0
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Box::new(|args, env| {
|
||||||
|
println!("args: {:?} env: {:?}", args, env);
|
||||||
|
0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
187
src/command.rs
Normal file
187
src/command.rs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/main.rs
160
src/main.rs
@@ -1,104 +1,108 @@
|
|||||||
#![feature(iter_intersperse)]
|
#![feature(iter_intersperse)]
|
||||||
|
#![feature(trait_alias)]
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
|
use command::{Command, CmdEnv, Args};
|
||||||
|
|
||||||
|
mod command;
|
||||||
|
mod cmd;
|
||||||
|
|
||||||
|
pub static SPECIAL_CHAR: char = '$';
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
||||||
let stdin = std::io::stdin();
|
let stdin = std::io::stdin();
|
||||||
let mut in_lock = stdin.lock();
|
let mut in_lock = stdin.lock();
|
||||||
|
|
||||||
let stdout = std::io::stdout();
|
let stdout = std::io::stdout();
|
||||||
let mut out_lock = stdout.lock();
|
let mut out_lock = stdout.lock();
|
||||||
|
|
||||||
|
let mut env= CmdEnv::new();
|
||||||
|
env.set("HOME", "/tmp/fake/dir");
|
||||||
|
env.set("?", "0");
|
||||||
|
// env.set("PATH", "/bin/");
|
||||||
|
|
||||||
|
let commands = &Command::all_commands(&env);
|
||||||
|
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();
|
||||||
|
|
||||||
in_lock.read_line(&mut buf).expect("Failed to read in to buffer.");
|
in_lock
|
||||||
let trim = buf.trim();
|
.read_line(&mut buf)
|
||||||
|
.expect("Failed to read in to buffer.");
|
||||||
|
|
||||||
if let Some(choice) = Command::choose(trim, &commands()) {
|
// TODO split at ' ' but don't split when it's surrounded by quotes '"'
|
||||||
let words = trim.split(' ').collect::<Vec<&str>>();
|
let args= &buf.trim().split(' ').map(|f| f.to_owned()).collect::<Vec<String>>();
|
||||||
choice.call(words);
|
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;
|
||||||
|
|
||||||
fn commands() -> Vec<Command> {
|
loop {
|
||||||
|
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, &mut env);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let commands = vec![
|
args = &args[1..];
|
||||||
Command::init("ls", vec![], nil),
|
options = &choice.sub_commands;
|
||||||
Command::init("exit", vec![], |_| {
|
|
||||||
std::process::exit(0);
|
|
||||||
}),
|
|
||||||
Command::init("echo", vec![], |f| {
|
|
||||||
let msg = &f[1..].iter().map(|s| s.to_owned()).intersperse(" ").collect::<String>();
|
|
||||||
println!("{msg}");
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
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.
|
||||||
type Param<'a> = Vec<&'a str>;
|
{
|
||||||
type Run = fn(Param) -> ();
|
choice.call(&args, &mut env);
|
||||||
fn nil(_: Param) {}
|
break;
|
||||||
|
}
|
||||||
#[derive(Debug)]
|
}
|
||||||
struct Command {
|
// When no good choice presents it's self
|
||||||
name: String,
|
None => {
|
||||||
sub_commands: Vec<Command>,
|
// perhaps they are trying to use builtins?
|
||||||
cmd: Run,
|
parse_lang(&args, &mut env);
|
||||||
}
|
break;
|
||||||
|
},
|
||||||
impl Command {
|
}
|
||||||
fn init(name: &str, sub_commands: Vec<Command>, f: Run) -> Self {
|
}
|
||||||
Self {
|
None => unimplemented!("Couldn't get index 0 of the args"),
|
||||||
name: name.to_string(),
|
|
||||||
sub_commands,
|
|
||||||
cmd: f,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&self, words: Param) {
|
|
||||||
let x = self.cmd;
|
|
||||||
x(words)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn choose<'a>(text: &str, choices: &'a Vec<Command>) -> Option<&'a Command> {
|
|
||||||
|
|
||||||
// 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
|
|
||||||
});
|
|
||||||
|
|
||||||
// sort by most matches
|
|
||||||
ranked.sort_by(|a,b| b.1.cmp(&a.1));
|
|
||||||
|
|
||||||
if ranked.len() > 1 {
|
|
||||||
// If the top two command choices are ranked the same, we don't know which to choose!
|
|
||||||
if ranked[0].1 == ranked[1].1 {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((cmd, _)) = ranked.first() {
|
|
||||||
// let words = text.split(' ').collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
return Some(cmd);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_lang(tokens: Args, env: &mut CmdEnv) {
|
||||||
|
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 isn't setting a variable (=)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user