Compare commits

...

12 Commits

Author SHA1 Message Date
6de062e808 info' 2025-08-16 11:58:11 -06:00
Oliver Atkinson
9cc1a64a91 add 0 support 2024-11-26 11:52:23 -07:00
Oliver Atkinson
e2df94a6c2 bash-like syntax 2024-11-26 11:45:53 -07:00
Oliver Atkinson
38b97f39b3 added env and simplifed command choosing system 2024-11-26 10:40:27 -07:00
Oliver Atkinson
c0fff34a7c fix command calling 2024-11-26 09:13:05 -07:00
Oliver Atkinson
00b4583f3b fix how commands are called 2024-11-26 09:12:48 -07:00
51c7528556 added stderr to problem solve 2024-11-25 22:04:34 -07:00
ea48cda85d clean up a bit 2024-11-25 20:42:11 -07:00
bc844cb5fa split up command into own file 2024-11-25 20:38:51 -07:00
c71ea179ec Merge pull request 'using_bin' (#1) from using_bin into master
Reviewed-on: #1
2024-11-26 03:03:23 +00:00
Oliver Atkinson
9af20c0424 built a better processor 2024-11-25 15:37:23 -07:00
Oliver Atkinson
e57e7ff907 added custom directory support
This allows you to specifcy things such as `/bin` to pull commands from.
It even kind of works
2024-11-25 14:10:51 -07:00
6 changed files with 339 additions and 78 deletions

20
bash_info.md Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
pub mod echo;
pub mod random;

31
src/cmd/random.rs Normal file
View 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
View 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
}
}

View File

@@ -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 (=)");
}
}
}