1use std::collections::HashMap;
2use std::env;
3use std::fs::File;
4use std::io::{self, Read, Write};
5use std::sync::{Arc, Mutex};
6use colored::*;
7pub use wasibox_core::IoContext;
8
9pub type CommandFn = Arc<dyn Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync>;
18
19pub struct CommandRegistry {
41 commands: HashMap<String, CommandFn>,
42 fallback: Option<CommandFn>,
43}
44
45impl CommandRegistry {
46 pub fn new() -> Self {
51 Self {
52 commands: HashMap::new(),
53 fallback: Some(Arc::new(|args: &[String], ctx: &mut IoContext| {
54 wasibox_core::execute_with_context(args.iter().cloned(), ctx)
55 })),
56 }
57 }
58
59 pub fn with_builtins() -> Self {
63 let mut reg = Self::new();
64
65 reg.register("help", |_args, ctx| {
66 writeln!(ctx.stdout, "{}", "Available Commands:".yellow().bold()).map_err(|e| e.to_string())?;
67 writeln!(ctx.stdout, " Shell Built-ins: cd, help, exit").map_err(|e| e.to_string())?;
68 writeln!(ctx.stdout, " Animations: sl").map_err(|e| e.to_string())?;
69 writeln!(ctx.stdout, " Core Utilities: arch, cat, cp, dir, echo, grep, head, ls, mkdir, mv, pwd, rm, rmdir, seq, sleep, tail, tee, touch, tree, uname, wc, whoami, yes, etc.").map_err(|e| e.to_string())?;
70 Ok(())
71 });
72
73 reg.register("cd", |args, _ctx| {
74 let new_dir = args.get(1).map(|s| s.as_str()).unwrap_or(".");
75 env::set_current_dir(new_dir).map_err(|e| format!("cd: {}", e))
76 });
77
78 reg.register("exit", |_args, _ctx| Ok(()));
79
80 reg.register("sl", |args, _ctx| {
81 let _ = sl::run(args.iter().cloned());
82 Ok(())
83 });
84
85 reg
86 }
87
88 pub fn register<F>(&mut self, name: impl Into<String>, handler: F)
90 where
91 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
92 {
93 self.commands.insert(name.into(), Arc::new(handler));
94 }
95
96 pub fn set_fallback<F>(&mut self, handler: F)
98 where
99 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
100 {
101 self.fallback = Some(Arc::new(handler));
102 }
103
104 pub fn remove_fallback(&mut self) {
106 self.fallback = None;
107 }
108
109 pub fn execute(&self, args: &[String], ctx: &mut IoContext) -> Result<(), String> {
111 if args.is_empty() {
112 return Ok(());
113 }
114 let cmd = &args[0];
115
116 if let Some(handler) = self.commands.get(cmd.as_str()) {
117 return handler(args, ctx);
118 }
119 if let Some(ref fallback) = self.fallback {
120 return fallback(args, ctx);
121 }
122 Err(format!("command not found: {}", cmd))
123 }
124}
125
126impl Default for CommandRegistry {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132fn is_broken_pipe(err: &str) -> bool {
138 err.contains("Broken pipe")
139 || err.contains("BrokenPipe")
140 || err.contains("broken pipe")
141}
142
143pub fn handle_pipeline(
148 line: &str,
149 initial_stdin: Box<dyn Read + Send + 'static>,
150 final_stdout: Box<dyn Write + Send + 'static>,
151 registry: &CommandRegistry,
152) -> Result<(), String> {
153 let stages_str: Vec<&str> = line.split('|').collect();
154
155 let mut final_stdout = Some(final_stdout);
156
157 std::thread::scope(|s| {
158 let mut prev_reader: Box<dyn Read + Send> = initial_stdin;
159 let mut threads = Vec::new();
160
161 for (i, stage_str) in stages_str.iter().enumerate() {
162 let is_last = i == stages_str.len() - 1;
163 let mut tokens = shlex::split(stage_str.trim())
164 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
165
166 let stdin = std::mem::replace(&mut prev_reader, Box::new(io::empty()));
167 let (stdout, next_reader): (Box<dyn Write + Send>, Option<Box<dyn Read + Send>>) = if is_last {
168 let mut out: Box<dyn Write + Send> = final_stdout.take().unwrap();
169 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
170 let append = tokens[pos] == ">>";
171 let filename = tokens.get(pos + 1).ok_or("Error: Missing file for redirection")?;
172 let file = if append {
173 std::fs::OpenOptions::new().create(true).append(true).open(filename)
174 } else {
175 File::create(filename)
176 }.map_err(|e| format!("Error opening file: {}", e))?;
177 out = Box::new(file);
178 tokens.truncate(pos);
179 }
180 (out, None)
181 } else {
182 let (reader, writer) = create_pipe();
183 (Box::new(writer), Some(Box::new(reader)))
184 };
185
186 if let Some(r) = next_reader {
187 prev_reader = r;
188 }
189
190 let thread = s.spawn(move || {
191 let mut ctx = IoContext::new(stdin, stdout, Box::new(io::stderr()));
192 registry.execute(&tokens, &mut ctx)
193 });
194 threads.push((i, thread));
195 }
196
197 let last_idx = stages_str.len() - 1;
200 let mut final_res = Ok(());
201 for (idx, thread) in threads {
202 match thread.join() {
203 Ok(Ok(())) => {}
204 Ok(Err(e)) => {
205 if idx != last_idx && is_broken_pipe(&e) {
206 continue;
207 }
208 if final_res.is_ok() {
209 final_res = Err(e);
210 }
211 }
212 Err(_) => {
213 if final_res.is_ok() {
214 final_res = Err("Thread panicked".to_string());
215 }
216 }
217 }
218 }
219 final_res
220 })
221}
222
223pub fn handle_parallel(
228 lines: Vec<String>,
229 initial_stdin: Box<dyn Read + Send + 'static>,
230 final_stdout: Box<dyn Write + Send + 'static>,
231 registry: Arc<CommandRegistry>,
232) -> Vec<Result<(), String>> {
233 let mut handles = Vec::new();
234 let mut stdin_opt: Option<Box<dyn Read + Send>> = Some(initial_stdin);
235 let mut stdout_opt: Option<Box<dyn Write + Send>> = Some(final_stdout);
236
237 for line in lines {
238 let registry = Arc::clone(®istry);
239 let stdin: Box<dyn Read + Send> = stdin_opt.take().unwrap_or_else(|| Box::new(io::empty()));
240 let stdout: Box<dyn Write + Send> = stdout_opt.take().unwrap_or_else(|| Box::new(io::stdout()));
241 let handle = std::thread::spawn(move || {
242 handle_pipeline(&line, stdin, stdout, &*registry)
243 });
244 handles.push(handle);
245 }
246
247 handles.into_iter()
248 .map(|h| h.join().unwrap_or(Err("Thread panicked".to_string())))
249 .collect()
250}
251
252struct PipeReader {
257 rx: std::sync::mpsc::Receiver<Vec<u8>>,
258 buffer: Vec<u8>,
259 pos: usize,
260}
261impl Read for PipeReader {
262 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
263 if self.pos >= self.buffer.len() {
264 match self.rx.recv() {
265 Ok(data) => {
266 self.buffer = data;
267 self.pos = 0;
268 }
269 Err(_) => return Ok(0), }
271 }
272 let available = self.buffer.len() - self.pos;
273 let to_copy = std::cmp::min(available, buf.len());
274 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
275 self.pos += to_copy;
276 Ok(to_copy)
277 }
278}
279
280struct PipeWriter {
281 tx: std::sync::mpsc::Sender<Vec<u8>>,
282}
283impl Write for PipeWriter {
284 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
285 self.tx.send(buf.to_vec()).map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?;
286 Ok(buf.len())
287 }
288 fn flush(&mut self) -> io::Result<()> { Ok(()) }
289}
290
291fn create_pipe() -> (PipeReader, PipeWriter) {
292 let (tx, rx) = std::sync::mpsc::channel();
293 (PipeReader { rx, buffer: Vec::new(), pos: 0 }, PipeWriter { tx })
294}
295
296pub struct ArcVecWriter {
302 pub inner: Arc<Mutex<Vec<u8>>>,
303}
304impl Write for ArcVecWriter {
305 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
306 let mut inner = self.inner.lock().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
307 inner.extend_from_slice(buf);
308 Ok(buf.len())
309 }
310 fn flush(&mut self) -> io::Result<()> { Ok(()) }
311}
312
313#[cfg(test)]
318mod tests {
319 use super::*;
320 use std::io::{BufRead, BufReader, Cursor};
321
322 fn get_temp_dir() -> tempfile::TempDir {
323 #[cfg(target_os = "wasi")]
324 {
325 tempfile::Builder::new()
326 .prefix("test_")
327 .tempdir_in(".")
328 .expect("Failed to create temp dir in current directory")
329 }
330 #[cfg(not(target_os = "wasi"))]
331 {
332 tempfile::tempdir().expect("Failed to create system temp dir")
333 }
334 }
335
336 fn builtins() -> CommandRegistry {
338 CommandRegistry::with_builtins()
339 }
340
341 #[test]
344 fn test_simple_echo() {
345 let out = Arc::new(Mutex::new(Vec::new()));
346 handle_pipeline("echo hello", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
347 let buf = out.lock().unwrap();
348 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
349 }
350
351 #[test]
352 fn test_pipe_echo_cat() {
353 let out = Arc::new(Mutex::new(Vec::new()));
354 handle_pipeline("echo hello | cat", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
355 let buf = out.lock().unwrap();
356 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
357 }
358
359 #[test]
360 fn test_pipe_grep() {
361 let out = Arc::new(Mutex::new(Vec::new()));
362 handle_pipeline("echo world | grep world", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
363 let buf = out.lock().unwrap();
364 assert_eq!(String::from_utf8_lossy(&buf).trim(), "world");
365 }
366
367 #[test]
368 fn test_redirection_create() {
369 let dir = get_temp_dir();
370 let file_path = dir.path().join("test_create.txt");
371 let cmd = format!("echo hello > \"{}\"", file_path.display());
372 handle_pipeline(&cmd, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins()).unwrap();
373 let content = std::fs::read_to_string(file_path).unwrap();
374 assert_eq!(content.trim(), "hello");
375 }
376
377 #[test]
378 fn test_redirection_append() {
379 let dir = get_temp_dir();
380 let file_path = dir.path().join("test_append.txt");
381 let cmd1 = format!("echo hello > \"{}\"", file_path.display());
382 handle_pipeline(&cmd1, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins()).unwrap();
383 let cmd2 = format!("echo world >> \"{}\"", file_path.display());
384 handle_pipeline(&cmd2, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins()).unwrap();
385 let content = std::fs::read_to_string(file_path).unwrap();
386 assert!(content.contains("hello"));
387 assert!(content.contains("world"));
388 }
389
390 #[test]
391 fn test_complex_pipeline() {
392 let out = Arc::new(Mutex::new(Vec::new()));
393 handle_pipeline("echo hello | grep h | wc -c", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
394 let buf = out.lock().unwrap();
395 assert_eq!(String::from_utf8_lossy(&buf).trim(), "6");
396 }
397
398 #[test]
401 fn test_custom_command() {
402 let mut reg = CommandRegistry::with_builtins();
403 reg.register("magic", |_args, ctx| {
404 write!(ctx.stdout, "magic happen").map_err(|e| e.to_string())
405 });
406
407 let out = Arc::new(Mutex::new(Vec::new()));
408 handle_pipeline("magic", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
409 let buf = out.lock().unwrap();
410 assert_eq!(String::from_utf8_lossy(&buf), "magic happen");
411 }
412
413 #[test]
414 fn test_custom_command_in_pipeline() {
415 let mut reg = CommandRegistry::with_builtins();
417 reg.register("double", |_args, ctx| {
418 for line in BufReader::new(&mut ctx.stdin).lines() {
419 let line = match line {
420 Ok(l) => l,
421 Err(_) => break,
422 };
423 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
424 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
425 }
426 Ok(())
427 });
428
429 let out = Arc::new(Mutex::new(Vec::new()));
430 handle_pipeline("echo hello | double", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
431 let buf = out.lock().unwrap();
432 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello\nhello");
433 }
434
435 #[test]
436 fn test_override_builtin() {
437 let mut reg = CommandRegistry::with_builtins();
439 reg.register("echo", |args, ctx| {
440 let msg = args[1..].join(" ").to_uppercase();
441 writeln!(ctx.stdout, "{}", msg).map_err(|e| e.to_string())
442 });
443
444 let out = Arc::new(Mutex::new(Vec::new()));
445 handle_pipeline("echo hello", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
446 let buf = out.lock().unwrap();
447 assert_eq!(String::from_utf8_lossy(&buf).trim(), "HELLO");
448 }
449
450 #[test]
453 fn test_all_external_pipeline() {
454 let mut reg = CommandRegistry::new();
455 reg.remove_fallback(); reg.register("count", |_args, ctx| {
459 for i in 1u64.. {
460 if writeln!(ctx.stdout, "{}", i).is_err() { break; }
461 }
462 Ok(())
463 });
464
465 reg.register("filter2", |_args, ctx| {
467 for line in BufReader::new(&mut ctx.stdin).lines() {
468 let line = match line {
469 Ok(l) => l,
470 Err(_) => break,
471 };
472 if line.contains('2') {
473 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
474 }
475 }
476 Ok(())
477 });
478
479 reg.register("take", |args, ctx| {
481 let n: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5);
482 for (i, line) in BufReader::new(&mut ctx.stdin).lines().enumerate() {
483 if i >= n { break; }
484 let line = match line {
485 Ok(l) => l,
486 Err(_) => break,
487 };
488 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
489 }
490 Ok(())
491 });
492
493 let out = Arc::new(Mutex::new(Vec::new()));
494 handle_pipeline(
495 "count | filter2 | take 3",
496 Box::new(io::empty()),
497 Box::new(ArcVecWriter { inner: Arc::clone(&out) }),
498 ®,
499 ).unwrap();
500
501 let buf = out.lock().unwrap();
502 let result = String::from_utf8_lossy(&buf);
503 let lines: Vec<&str> = result.trim().lines().collect();
504 assert_eq!(lines.len(), 3);
505 for line in &lines {
506 assert!(line.contains('2'), "expected '2' in line, got: {}", line);
507 }
508 assert_eq!(lines[0], "2");
509 assert_eq!(lines[1], "12");
510 assert_eq!(lines[2], "20");
511 }
512
513 #[test]
516 fn test_parallel_execution() {
517 let mut reg = CommandRegistry::with_builtins();
518 reg.register("slow", |_args, _ctx| {
519 std::thread::sleep(std::time::Duration::from_millis(100));
520 Ok(())
521 });
522 let registry = Arc::new(reg);
523
524 let lines = vec!["slow".to_string(), "slow".to_string(), "slow".to_string()];
525 let start = std::time::Instant::now();
526 let results = handle_parallel(lines, Box::new(io::empty()), Box::new(io::sink()), registry);
527 let duration = start.elapsed();
528
529 assert_eq!(results.len(), 3);
530 for res in results {
531 assert!(res.is_ok());
532 }
533 assert!(duration < std::time::Duration::from_millis(250));
534 }
535
536 #[test]
539 fn test_streaming_pipeline() {
540 let out = Arc::new(Mutex::new(Vec::new()));
541 handle_pipeline("yes | head -n 2", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
542 let buf = out.lock().unwrap();
543 assert_eq!(String::from_utf8_lossy(&buf).trim(), "y\ny");
544 }
545
546 #[test]
547 fn test_seq_pipeline_head() {
548 let out = Arc::new(Mutex::new(Vec::new()));
549 handle_pipeline("seq | head -n 5", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
550 let buf = out.lock().unwrap();
551 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3\n4\n5");
552 }
553
554 #[test]
555 fn test_seq_pipeline_grep_head() {
556 let out = Arc::new(Mutex::new(Vec::new()));
557 handle_pipeline("seq | grep 2 | head -n 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
558 let buf = out.lock().unwrap();
559 let result = String::from_utf8_lossy(&buf);
560 let lines: Vec<&str> = result.trim().lines().collect();
561 assert_eq!(lines.len(), 3);
562 for line in &lines {
563 assert!(line.contains('2'));
564 }
565 assert_eq!(lines[0], "2");
566 assert_eq!(lines[1], "12");
567 assert_eq!(lines[2], "20");
568 }
569
570 #[test]
571 fn test_seq_finite() {
572 let out = Arc::new(Mutex::new(Vec::new()));
573 handle_pipeline("seq 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
574 let buf = out.lock().unwrap();
575 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3");
576 }
577
578 #[test]
579 fn test_seq_range() {
580 let out = Arc::new(Mutex::new(Vec::new()));
581 handle_pipeline("seq 5 8", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
582 let buf = out.lock().unwrap();
583 assert_eq!(String::from_utf8_lossy(&buf).trim(), "5\n6\n7\n8");
584 }
585
586 #[test]
587 fn test_seq_step() {
588 let out = Arc::new(Mutex::new(Vec::new()));
589 handle_pipeline("seq 1 2 10", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
590 let buf = out.lock().unwrap();
591 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n3\n5\n7\n9");
592 }
593 #[test]
594 fn test_cd_and_ls() {
595 let dir = get_temp_dir();
596 std::fs::write(dir.path().join("aaa.txt"), "hello").unwrap();
598 std::fs::write(dir.path().join("bbb.txt"), "world").unwrap();
599
600 let original_cwd = env::current_dir().unwrap();
601 let reg = builtins();
602
603 let cd_cmd = format!("cd \"{}\"", dir.path().display());
605 handle_pipeline(&cd_cmd, Box::new(io::empty()), Box::new(io::sink()), ®).unwrap();
606
607 let out = Arc::new(Mutex::new(Vec::new()));
609 handle_pipeline("ls", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
610
611 let buf = out.lock().unwrap();
612 let output = String::from_utf8_lossy(&buf);
613 assert!(output.contains("aaa.txt"), "expected aaa.txt in ls output, got: {}", output);
614 assert!(output.contains("bbb.txt"), "expected bbb.txt in ls output, got: {}", output);
615
616 env::set_current_dir(original_cwd).unwrap();
618 }
619}