use crate::filesystem::{EntryKind, Filesystem};
use super::host::Output;
pub async fn dispatch(fs: &dyn Filesystem, cmd: &str, args: &[String], stdin: &str) -> Output {
dispatch_in(fs, "/", cmd, args, stdin).await
}
pub async fn dispatch_in(
fs: &dyn Filesystem,
cwd: &str,
cmd: &str,
args: &[String],
stdin: &str,
) -> Output {
match cmd {
"echo" => echo(args),
"true" => Output::ok(""),
"false" => Output { stdout: String::new(), stderr: String::new(), code: 1 },
"[" | "test" => test_cmd(fs, cwd, cmd, args).await,
"ls" => ls(fs, cwd, args).await,
"cat" => cat(fs, cwd, args).await,
"wc" => Output::ok(wc(args, stdin)),
"grep" => grep(args, stdin),
"head" => head_tail(args, stdin, true),
"tail" => head_tail(args, stdin, false),
"find" => find(fs, cwd, args).await,
"mkdir" => mkdir(fs, cwd, args).await,
"write" | "create" => write_create(fs, cwd, args).await,
other => Output::err(format!("{other}: command not found"), 127),
}
}
fn echo(args: &[String]) -> Output {
let (no_newline, rest) = match args.first() {
Some(f) if f == "-n" => (true, &args[1..]),
_ => (false, args),
};
let mut s = rest.join(" ");
if !no_newline {
s.push('\n');
}
Output::ok(s)
}
async fn ls(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
let targets: Vec<String> =
if args.is_empty() { vec![cwd.to_string()] } else { args.iter().map(|a| resolve(cwd, a)).collect() };
let multi = targets.len() > 1;
let mut out = String::new();
let mut code = 0;
for (i, t) in targets.iter().enumerate() {
match fs.metadata(t).await {
Ok(Some(m)) if m.kind == EntryKind::Directory => {
match fs.read_dir(t).await {
Ok(mut entries) => {
entries.sort_by(|a, b| a.name.cmp(&b.name));
if multi {
out.push_str(&format!("{}:\n", display_path(t)));
}
for e in entries {
out.push_str(&e.name);
out.push('\n');
}
if multi && i + 1 < targets.len() {
out.push('\n');
}
}
Err(e) => {
return Output::err(format!("ls: {}: {e}", display_path(t)), 1);
}
}
}
Ok(Some(_)) => {
out.push_str(&display_path(t));
out.push('\n');
}
Ok(None) => {
return Output::err(format!("ls: {}: no such file or directory", display_path(t)), 1);
}
Err(e) => {
code = 1;
out.push_str(&format!("ls: {}: {e}\n", display_path(t)));
}
}
}
Output { stdout: out, stderr: String::new(), code }
}
async fn cat(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
if args.is_empty() {
return Output::err("cat: no file given", 1);
}
let mut out = String::new();
for a in args {
let p = resolve(cwd, a);
match fs.read(&p).await {
Ok(bytes) => out.push_str(&String::from_utf8_lossy(&bytes)),
Err(e) => return Output::err(format!("cat: {a}: {e}"), 1),
}
}
Output::ok(out)
}
fn wc(args: &[String], stdin: &str) -> String {
let lines = stdin.lines().count();
let words = stdin.split_whitespace().count();
let bytes = stdin.len();
match args.first().map(String::as_str) {
Some("-l") => format!("{lines}\n"),
Some("-w") => format!("{words}\n"),
Some("-c") => format!("{bytes}\n"),
_ => format!("{lines} {words} {bytes}\n"),
}
}
fn grep(args: &[String], stdin: &str) -> Output {
let mut invert = false;
let mut ignore_case = false;
let mut count = false;
let mut pattern: Option<&str> = None;
for a in args {
match a.as_str() {
"-v" => invert = true,
"-i" => ignore_case = true,
"-c" => count = true,
s if s.starts_with('-') && s.len() > 1 && s[1..].chars().all(|c| "vic".contains(c)) => {
for c in s[1..].chars() {
match c {
'v' => invert = true,
'i' => ignore_case = true,
'c' => count = true,
_ => {}
}
}
}
_ => {
if pattern.is_none() {
pattern = Some(a);
}
}
}
}
let Some(pat) = pattern else {
return Output::err("grep: no pattern given", 2);
};
let needle = if ignore_case { pat.to_lowercase() } else { pat.to_string() };
let mut matched = Vec::new();
for line in stdin.lines() {
let hay = if ignore_case { line.to_lowercase() } else { line.to_string() };
let hit = hay.contains(&needle);
if hit != invert {
matched.push(line);
}
}
let code = if matched.is_empty() { 1 } else { 0 };
if count {
return Output { stdout: format!("{}\n", matched.len()), stderr: String::new(), code };
}
let mut out = matched.join("\n");
if !out.is_empty() {
out.push('\n');
}
Output { stdout: out, stderr: String::new(), code }
}
async fn find(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
let mut root: Option<String> = None;
let mut name_glob: Option<String> = None;
let mut type_filter: Option<EntryKind> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-name" => {
i += 1;
match args.get(i) {
Some(g) => name_glob = Some(g.clone()),
None => return Output::err("find: -name needs an argument", 1),
}
}
"-type" => {
i += 1;
match args.get(i).map(String::as_str) {
Some("f") => type_filter = Some(EntryKind::File),
Some("d") => type_filter = Some(EntryKind::Directory),
_ => return Output::err("find: -type expects f or d", 1),
}
}
other if root.is_none() && !other.starts_with('-') => root = Some(resolve(cwd, other)),
other => return Output::err(format!("find: unknown argument: {other}"), 1),
}
i += 1;
}
let root = root.unwrap_or_else(|| cwd.to_string());
match fs.walk(&root, None).await {
Ok(mut entries) => {
entries.sort_by(|a, b| a.path.cmp(&b.path));
let mut out = String::new();
for e in entries {
if let Some(t) = type_filter {
if e.kind != t {
continue;
}
}
if let Some(g) = &name_glob {
let name = crate::filesystem::file_name(&e.path);
if !glob_match(g, name) {
continue;
}
}
out.push_str(&display_path(&e.path));
out.push('\n');
}
Output::ok(out)
}
Err(e) => Output::err(format!("find: {}: {e}", display_path(&root)), 1),
}
}
async fn mkdir(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
if args.is_empty() {
return Output::err("mkdir: no directory given", 1);
}
for a in args {
if a == "-p" {
continue;
}
let dir = resolve(cwd, a);
let marker = format!("{}/.keep", dir.trim_end_matches('/'));
if let Err(e) = fs.write_atomic(&marker, b"").await {
return Output::err(format!("mkdir: {a}: {e}"), 1);
}
}
Output::ok("")
}
fn head_tail(args: &[String], stdin: &str, from_head: bool) -> Output {
let n = match parse_line_count(args) {
Ok(n) => n,
Err(msg) => return Output::err(msg, 2),
};
let lines: Vec<&str> = stdin.lines().collect();
let selected: &[&str] = if from_head {
&lines[..lines.len().min(n)]
} else {
&lines[lines.len().saturating_sub(n)..]
};
let mut out = selected.join("\n");
if !out.is_empty() {
out.push('\n');
}
Output::ok(out)
}
fn parse_line_count(args: &[String]) -> Result<usize, String> {
let count = |s: &str| s.parse::<usize>().map_err(|_| format!("head/tail: invalid line count: {s}"));
match args {
[] => Ok(10),
[a, b] if a == "-n" => count(b),
[a] if a == "-n" => Err("head/tail: option requires an argument: -n".to_string()),
[a] if a.starts_with("-n") => count(&a[2..]),
[a] if a.starts_with('-') && a.len() > 1 => count(&a[1..]),
_ => Err("head/tail: usage: head|tail [-n N] (reads stdin)".to_string()),
}
}
async fn write_create(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
let Some(path) = args.first() else {
return Output::err("write: usage: write PATH [CONTENT...]", 1);
};
let p = resolve(cwd, path);
match fs.metadata(&p).await {
Ok(Some(_)) => {
return Output::err(format!("write: {path}: already exists (create-only in v1)"), 1)
}
Ok(None) => {}
Err(e) => return Output::err(format!("write: {path}: {e}"), 1),
}
let content = args[1..].join(" ");
match fs.write_atomic(&p, content.as_bytes()).await {
Ok(()) => Output::ok(""),
Err(e) => Output::err(format!("write: {path}: {e}"), 1),
}
}
async fn test_cmd(fs: &dyn Filesystem, cwd: &str, cmd: &str, args: &[String]) -> Output {
let operands: Vec<&str> = if cmd == "[" {
match args.last() {
Some(last) if last == "]" => args[..args.len() - 1].iter().map(String::as_str).collect(),
_ => return Output::err("[: missing closing `]`", 2),
}
} else {
args.iter().map(String::as_str).collect()
};
if operands.len() == 2 && matches!(operands[0], "-e" | "-f" | "-d") {
let resolved = resolve(cwd, operands[1]);
let truthy = match fs.metadata(&resolved).await {
Ok(Some(m)) => match operands[0] {
"-e" => true,
"-f" => m.kind == EntryKind::File,
"-d" => m.kind == EntryKind::Directory,
_ => false,
},
_ => false,
};
return Output { stdout: String::new(), stderr: String::new(), code: i32::from(!truthy) };
}
match eval_test(&operands) {
Ok(true) => Output { stdout: String::new(), stderr: String::new(), code: 0 },
Ok(false) => Output { stdout: String::new(), stderr: String::new(), code: 1 },
Err(msg) => Output::err(format!("{cmd}: {msg}"), 2),
}
}
fn eval_test(ops: &[&str]) -> Result<bool, String> {
match ops {
[] => Ok(false),
[a] => Ok(!a.is_empty()),
["-z", a] => Ok(a.is_empty()),
["-n", a] => Ok(!a.is_empty()),
[a, op, b] => match *op {
"=" | "==" => Ok(a == b),
"!=" => Ok(a != b),
"-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge" => {
let x: i64 = a.parse().map_err(|_| format!("integer expected: {a}"))?;
let y: i64 = b.parse().map_err(|_| format!("integer expected: {b}"))?;
Ok(match *op {
"-eq" => x == y,
"-ne" => x != y,
"-lt" => x < y,
"-le" => x <= y,
"-gt" => x > y,
"-ge" => x >= y,
_ => unreachable!(),
})
}
other => Err(format!("unknown operator: {other}")),
},
_ => Err("too many arguments".to_string()),
}
}
pub(crate) fn resolve(cwd: &str, path: &str) -> String {
let base = if path.starts_with('/') { String::new() } else { cwd.to_string() };
let combined = format!("{base}/{path}");
let mut stack: Vec<&str> = Vec::new();
for comp in combined.split('/') {
match comp {
"" | "." => {}
".." => {
stack.pop();
}
c => stack.push(c),
}
}
if stack.is_empty() {
"/".to_string()
} else {
format!("/{}", stack.join("/"))
}
}
fn display_path(p: &str) -> String {
match p.strip_prefix('/') {
Some("") | None => p.to_string(),
Some(rest) => rest.to_string(),
}
}
pub(crate) fn glob_match(pat: &str, name: &str) -> bool {
let p: Vec<char> = pat.chars().collect();
let n: Vec<char> = name.chars().collect();
glob_rec(&p, &n)
}
fn glob_rec(p: &[char], n: &[char]) -> bool {
match p.first() {
None => n.is_empty(),
Some('*') => {
glob_rec(&p[1..], n) || (!n.is_empty() && glob_rec(p, &n[1..]))
}
Some('?') => !n.is_empty() && glob_rec(&p[1..], &n[1..]),
Some(c) => n.first() == Some(c) && glob_rec(&p[1..], &n[1..]),
}
}