1use crate::filesystem::{EntryKind, Filesystem};
14
15use super::host::Output;
16
17pub async fn dispatch(fs: &dyn Filesystem, cmd: &str, args: &[String], stdin: &str) -> Output {
21 dispatch_in(fs, "/", cmd, args, stdin).await
22}
23
24pub async fn dispatch_in(
26 fs: &dyn Filesystem,
27 cwd: &str,
28 cmd: &str,
29 args: &[String],
30 stdin: &str,
31) -> Output {
32 match cmd {
33 "echo" => echo(args),
34 "true" => Output::ok(""),
35 "false" => Output { stdout: String::new(), stderr: String::new(), code: 1 },
36 "[" | "test" => test_cmd(fs, cwd, cmd, args).await,
37 "ls" => ls(fs, cwd, args).await,
38 "cat" => cat(fs, cwd, args).await,
39 "wc" => Output::ok(wc(args, stdin)),
40 "grep" => grep(args, stdin),
41 "head" => head_tail(args, stdin, true),
42 "tail" => head_tail(args, stdin, false),
43 "find" => find(fs, cwd, args).await,
44 "mkdir" => mkdir(fs, cwd, args).await,
45 "write" | "create" => write_create(fs, cwd, args).await,
46 other => Output::err(format!("{other}: command not found"), 127),
47 }
48}
49
50fn echo(args: &[String]) -> Output {
52 let (no_newline, rest) = match args.first() {
53 Some(f) if f == "-n" => (true, &args[1..]),
54 _ => (false, args),
55 };
56 let mut s = rest.join(" ");
57 if !no_newline {
58 s.push('\n');
59 }
60 Output::ok(s)
61}
62
63async fn ls(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
66 let targets: Vec<String> =
67 if args.is_empty() { vec![cwd.to_string()] } else { args.iter().map(|a| resolve(cwd, a)).collect() };
68 let multi = targets.len() > 1;
69 let mut out = String::new();
70 let mut code = 0;
71 for (i, t) in targets.iter().enumerate() {
72 match fs.metadata(t).await {
73 Ok(Some(m)) if m.kind == EntryKind::Directory => {
74 match fs.read_dir(t).await {
75 Ok(mut entries) => {
76 entries.sort_by(|a, b| a.name.cmp(&b.name));
77 if multi {
78 out.push_str(&format!("{}:\n", display_path(t)));
79 }
80 for e in entries {
81 out.push_str(&e.name);
82 out.push('\n');
83 }
84 if multi && i + 1 < targets.len() {
85 out.push('\n');
86 }
87 }
88 Err(e) => {
89 return Output::err(format!("ls: {}: {e}", display_path(t)), 1);
90 }
91 }
92 }
93 Ok(Some(_)) => {
94 out.push_str(&display_path(t));
96 out.push('\n');
97 }
98 Ok(None) => {
99 return Output::err(format!("ls: {}: no such file or directory", display_path(t)), 1);
100 }
101 Err(e) => {
102 code = 1;
103 out.push_str(&format!("ls: {}: {e}\n", display_path(t)));
104 }
105 }
106 }
107 Output { stdout: out, stderr: String::new(), code }
108}
109
110async fn cat(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
112 if args.is_empty() {
113 return Output::err("cat: no file given", 1);
114 }
115 let mut out = String::new();
116 for a in args {
117 let p = resolve(cwd, a);
118 match fs.read(&p).await {
119 Ok(bytes) => out.push_str(&String::from_utf8_lossy(&bytes)),
120 Err(e) => return Output::err(format!("cat: {a}: {e}"), 1),
121 }
122 }
123 Output::ok(out)
124}
125
126fn wc(args: &[String], stdin: &str) -> String {
129 let lines = stdin.lines().count();
130 let words = stdin.split_whitespace().count();
131 let bytes = stdin.len();
132 match args.first().map(String::as_str) {
133 Some("-l") => format!("{lines}\n"),
134 Some("-w") => format!("{words}\n"),
135 Some("-c") => format!("{bytes}\n"),
136 _ => format!("{lines} {words} {bytes}\n"),
137 }
138}
139
140fn grep(args: &[String], stdin: &str) -> Output {
145 let mut invert = false;
146 let mut ignore_case = false;
147 let mut count = false;
148 let mut pattern: Option<&str> = None;
149 for a in args {
150 match a.as_str() {
151 "-v" => invert = true,
152 "-i" => ignore_case = true,
153 "-c" => count = true,
154 s if s.starts_with('-') && s.len() > 1 && s[1..].chars().all(|c| "vic".contains(c)) => {
156 for c in s[1..].chars() {
157 match c {
158 'v' => invert = true,
159 'i' => ignore_case = true,
160 'c' => count = true,
161 _ => {}
162 }
163 }
164 }
165 _ => {
166 if pattern.is_none() {
167 pattern = Some(a);
168 }
169 }
170 }
171 }
172 let Some(pat) = pattern else {
173 return Output::err("grep: no pattern given", 2);
174 };
175 let needle = if ignore_case { pat.to_lowercase() } else { pat.to_string() };
176 let mut matched = Vec::new();
177 for line in stdin.lines() {
178 let hay = if ignore_case { line.to_lowercase() } else { line.to_string() };
179 let hit = hay.contains(&needle);
180 if hit != invert {
181 matched.push(line);
182 }
183 }
184 let code = if matched.is_empty() { 1 } else { 0 };
188 if count {
189 return Output { stdout: format!("{}\n", matched.len()), stderr: String::new(), code };
190 }
191 let mut out = matched.join("\n");
192 if !out.is_empty() {
193 out.push('\n');
194 }
195 Output { stdout: out, stderr: String::new(), code }
196}
197
198async fn find(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
201 let mut root: Option<String> = None;
202 let mut name_glob: Option<String> = None;
203 let mut type_filter: Option<EntryKind> = None;
204 let mut i = 0;
205 while i < args.len() {
206 match args[i].as_str() {
207 "-name" => {
208 i += 1;
209 match args.get(i) {
210 Some(g) => name_glob = Some(g.clone()),
211 None => return Output::err("find: -name needs an argument", 1),
212 }
213 }
214 "-type" => {
215 i += 1;
216 match args.get(i).map(String::as_str) {
217 Some("f") => type_filter = Some(EntryKind::File),
218 Some("d") => type_filter = Some(EntryKind::Directory),
219 _ => return Output::err("find: -type expects f or d", 1),
220 }
221 }
222 other if root.is_none() && !other.starts_with('-') => root = Some(resolve(cwd, other)),
223 other => return Output::err(format!("find: unknown argument: {other}"), 1),
224 }
225 i += 1;
226 }
227 let root = root.unwrap_or_else(|| cwd.to_string());
228 match fs.walk(&root, None).await {
229 Ok(mut entries) => {
230 entries.sort_by(|a, b| a.path.cmp(&b.path));
231 let mut out = String::new();
232 for e in entries {
233 if let Some(t) = type_filter {
234 if e.kind != t {
235 continue;
236 }
237 }
238 if let Some(g) = &name_glob {
239 let name = crate::filesystem::file_name(&e.path);
240 if !glob_match(g, name) {
241 continue;
242 }
243 }
244 out.push_str(&display_path(&e.path));
245 out.push('\n');
246 }
247 Output::ok(out)
248 }
249 Err(e) => Output::err(format!("find: {}: {e}", display_path(&root)), 1),
250 }
251}
252
253async fn mkdir(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
257 if args.is_empty() {
258 return Output::err("mkdir: no directory given", 1);
259 }
260 for a in args {
261 if a == "-p" {
263 continue;
264 }
265 let dir = resolve(cwd, a);
266 let marker = format!("{}/.keep", dir.trim_end_matches('/'));
267 if let Err(e) = fs.write_atomic(&marker, b"").await {
268 return Output::err(format!("mkdir: {a}: {e}"), 1);
269 }
270 }
271 Output::ok("")
272}
273
274fn head_tail(args: &[String], stdin: &str, from_head: bool) -> Output {
278 let n = match parse_line_count(args) {
279 Ok(n) => n,
280 Err(msg) => return Output::err(msg, 2),
281 };
282 let lines: Vec<&str> = stdin.lines().collect();
283 let selected: &[&str] = if from_head {
284 &lines[..lines.len().min(n)]
285 } else {
286 &lines[lines.len().saturating_sub(n)..]
287 };
288 let mut out = selected.join("\n");
289 if !out.is_empty() {
290 out.push('\n');
291 }
292 Output::ok(out)
293}
294
295fn parse_line_count(args: &[String]) -> Result<usize, String> {
298 let count = |s: &str| s.parse::<usize>().map_err(|_| format!("head/tail: invalid line count: {s}"));
299 match args {
300 [] => Ok(10),
301 [a, b] if a == "-n" => count(b),
302 [a] if a == "-n" => Err("head/tail: option requires an argument: -n".to_string()),
303 [a] if a.starts_with("-n") => count(&a[2..]),
304 [a] if a.starts_with('-') && a.len() > 1 => count(&a[1..]),
305 _ => Err("head/tail: usage: head|tail [-n N] (reads stdin)".to_string()),
306 }
307}
308
309async fn write_create(fs: &dyn Filesystem, cwd: &str, args: &[String]) -> Output {
314 let Some(path) = args.first() else {
315 return Output::err("write: usage: write PATH [CONTENT...]", 1);
316 };
317 let p = resolve(cwd, path);
318 match fs.metadata(&p).await {
319 Ok(Some(_)) => {
320 return Output::err(format!("write: {path}: already exists (create-only in v1)"), 1)
321 }
322 Ok(None) => {}
323 Err(e) => return Output::err(format!("write: {path}: {e}"), 1),
324 }
325 let content = args[1..].join(" ");
326 match fs.write_atomic(&p, content.as_bytes()).await {
327 Ok(()) => Output::ok(""),
328 Err(e) => Output::err(format!("write: {path}: {e}"), 1),
329 }
330}
331
332async fn test_cmd(fs: &dyn Filesystem, cwd: &str, cmd: &str, args: &[String]) -> Output {
339 let operands: Vec<&str> = if cmd == "[" {
341 match args.last() {
342 Some(last) if last == "]" => args[..args.len() - 1].iter().map(String::as_str).collect(),
343 _ => return Output::err("[: missing closing `]`", 2),
344 }
345 } else {
346 args.iter().map(String::as_str).collect()
347 };
348 if operands.len() == 2 && matches!(operands[0], "-e" | "-f" | "-d") {
350 let resolved = resolve(cwd, operands[1]);
351 let truthy = match fs.metadata(&resolved).await {
352 Ok(Some(m)) => match operands[0] {
353 "-e" => true,
354 "-f" => m.kind == EntryKind::File,
355 "-d" => m.kind == EntryKind::Directory,
356 _ => false,
357 },
358 _ => false,
360 };
361 return Output { stdout: String::new(), stderr: String::new(), code: i32::from(!truthy) };
362 }
363 match eval_test(&operands) {
364 Ok(true) => Output { stdout: String::new(), stderr: String::new(), code: 0 },
365 Ok(false) => Output { stdout: String::new(), stderr: String::new(), code: 1 },
366 Err(msg) => Output::err(format!("{cmd}: {msg}"), 2),
367 }
368}
369
370fn eval_test(ops: &[&str]) -> Result<bool, String> {
371 match ops {
372 [] => Ok(false),
374 [a] => Ok(!a.is_empty()),
376 ["-z", a] => Ok(a.is_empty()),
378 ["-n", a] => Ok(!a.is_empty()),
379 [a, op, b] => match *op {
381 "=" | "==" => Ok(a == b),
382 "!=" => Ok(a != b),
383 "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge" => {
384 let x: i64 = a.parse().map_err(|_| format!("integer expected: {a}"))?;
385 let y: i64 = b.parse().map_err(|_| format!("integer expected: {b}"))?;
386 Ok(match *op {
387 "-eq" => x == y,
388 "-ne" => x != y,
389 "-lt" => x < y,
390 "-le" => x <= y,
391 "-gt" => x > y,
392 "-ge" => x >= y,
393 _ => unreachable!(),
394 })
395 }
396 other => Err(format!("unknown operator: {other}")),
397 },
398 _ => Err("too many arguments".to_string()),
399 }
400}
401
402pub(crate) fn resolve(cwd: &str, path: &str) -> String {
411 let base = if path.starts_with('/') { String::new() } else { cwd.to_string() };
412 let combined = format!("{base}/{path}");
413 let mut stack: Vec<&str> = Vec::new();
414 for comp in combined.split('/') {
415 match comp {
416 "" | "." => {}
417 ".." => {
418 stack.pop();
419 }
420 c => stack.push(c),
421 }
422 }
423 if stack.is_empty() {
424 "/".to_string()
425 } else {
426 format!("/{}", stack.join("/"))
427 }
428}
429
430fn display_path(p: &str) -> String {
433 match p.strip_prefix('/') {
434 Some("") | None => p.to_string(),
435 Some(rest) => rest.to_string(),
436 }
437}
438
439pub(crate) fn glob_match(pat: &str, name: &str) -> bool {
442 let p: Vec<char> = pat.chars().collect();
443 let n: Vec<char> = name.chars().collect();
444 glob_rec(&p, &n)
445}
446
447fn glob_rec(p: &[char], n: &[char]) -> bool {
448 match p.first() {
449 None => n.is_empty(),
450 Some('*') => {
451 glob_rec(&p[1..], n) || (!n.is_empty() && glob_rec(p, &n[1..]))
453 }
454 Some('?') => !n.is_empty() && glob_rec(&p[1..], &n[1..]),
455 Some(c) => n.first() == Some(c) && glob_rec(&p[1..], &n[1..]),
456 }
457}