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
68 let mut shell_builtins = vec!["cd", "help", "exit"];
69 #[cfg(feature = "clear")] shell_builtins.push("clear");
70 shell_builtins.sort();
71 writeln!(ctx.stdout, " Shell Built-ins: {}", shell_builtins.join(", ")).map_err(|e| e.to_string())?;
72
73 #[cfg(feature = "sl")]
74 writeln!(ctx.stdout, " Animations: sl").map_err(|e| e.to_string())?;
75
76 let mut utils = Vec::new();
77 #[cfg(feature = "arch")] utils.push("arch");
78 #[cfg(feature = "basename")] utils.push("basename");
79 #[cfg(feature = "cat")] utils.push("cat");
80 #[cfg(feature = "cp")] utils.push("cp");
81 #[cfg(feature = "dir")] utils.push("dir");
82 #[cfg(feature = "dirname")] utils.push("dirname");
83 #[cfg(feature = "echo")] utils.push("echo");
84 #[cfg(feature = "env")] utils.push("env");
85 #[cfg(feature = "false")] utils.push("false");
86 #[cfg(feature = "grep")] utils.push("grep");
87 #[cfg(feature = "head")] utils.push("head");
88 #[cfg(feature = "link")] utils.push("link");
89 #[cfg(feature = "ln")] utils.push("ln");
90 #[cfg(feature = "ls")] utils.push("ls");
91 #[cfg(feature = "mkdir")] utils.push("mkdir");
92 #[cfg(feature = "mv")] utils.push("mv");
93 #[cfg(feature = "pwd")] utils.push("pwd");
94 #[cfg(feature = "rm")] utils.push("rm");
95 #[cfg(feature = "rmdir")] utils.push("rmdir");
96 #[cfg(feature = "seq")] utils.push("seq");
97 #[cfg(feature = "sleep")] utils.push("sleep");
98 #[cfg(feature = "tail")] utils.push("tail");
99 #[cfg(feature = "tee")] utils.push("tee");
100 #[cfg(feature = "touch")] utils.push("touch");
101 #[cfg(feature = "tree")] utils.push("tree");
102 #[cfg(feature = "true")] utils.push("true");
103 #[cfg(feature = "uname")] utils.push("uname");
104 #[cfg(feature = "unlink")] utils.push("unlink");
105 #[cfg(feature = "wc")] utils.push("wc");
106 #[cfg(feature = "whoami")] utils.push("whoami");
107 #[cfg(feature = "yes")] utils.push("yes");
108
109 if !utils.is_empty() {
110 utils.sort();
111 writeln!(ctx.stdout, " Core Utilities: {}", utils.join(", ")).map_err(|e| e.to_string())?;
112 }
113 Ok(())
114 });
115
116 reg.register("cd", |args, _ctx| {
117 let new_dir = args.get(1).map(|s| s.as_str()).unwrap_or(".");
118 env::set_current_dir(new_dir).map_err(|e| format!("cd: {}", e))
119 });
120
121 reg.register("exit", |_args, _ctx| Ok(()));
122
123 #[cfg(feature = "clear")]
124 reg.register("clear", |_args, ctx| {
125 write!(ctx.stdout, "\x1B[2J\x1B[1;1H").map_err(|e| e.to_string())?;
126 ctx.stdout.flush().map_err(|e| e.to_string())
127 });
128
129 #[cfg(feature = "sl")]
130 reg.register("sl", |args, _ctx| {
131 let _ = sl::run(args.iter().cloned());
132 Ok(())
133 });
134
135 #[cfg(feature = "arch")]
137 reg.register("arch", |args, ctx| wasibox_core::utils::arch::execute_with_context(args.iter().cloned(), ctx));
138 #[cfg(feature = "basename")]
139 reg.register("basename", |args, ctx| wasibox_core::utils::basename::execute_with_context(args.iter().cloned(), ctx));
140 #[cfg(feature = "cat")]
141 reg.register("cat", |args, ctx| wasibox_core::utils::cat::execute_with_context(args.iter().cloned(), ctx));
142 #[cfg(feature = "cp")]
143 reg.register("cp", |args, ctx| wasibox_core::utils::cp::execute_with_context(args.iter().cloned(), ctx));
144 #[cfg(feature = "dir")]
145 reg.register("dir", |args, ctx| wasibox_core::utils::dir::execute_with_context(args.iter().cloned(), ctx));
146 #[cfg(feature = "dirname")]
147 reg.register("dirname", |args, ctx| wasibox_core::utils::dirname::execute_with_context(args.iter().cloned(), ctx));
148 #[cfg(feature = "echo")]
149 reg.register("echo", |args, ctx| wasibox_core::utils::echo::execute_with_context(args.iter().cloned(), ctx));
150 #[cfg(feature = "env")]
151 reg.register("env", |args, ctx| wasibox_core::utils::env::execute_with_context(args.iter().cloned(), ctx));
152 #[cfg(feature = "false")]
153 reg.register("false", |args, ctx| wasibox_core::utils::r#false::execute_with_context(args.iter().cloned(), ctx));
154 #[cfg(feature = "grep")]
155 reg.register("grep", |args, ctx| wasibox_core::utils::grep::execute_with_context(args.iter().cloned(), ctx));
156 #[cfg(feature = "head")]
157 reg.register("head", |args, ctx| wasibox_core::utils::head::execute_with_context(args.iter().cloned(), ctx));
158 #[cfg(feature = "link")]
159 reg.register("link", |args, ctx| wasibox_core::utils::link::execute_with_context(args.iter().cloned(), ctx));
160 #[cfg(feature = "ln")]
161 reg.register("ln", |args, ctx| wasibox_core::utils::ln::execute_with_context(args.iter().cloned(), ctx));
162 #[cfg(feature = "ls")]
163 reg.register("ls", |args, ctx| wasibox_core::utils::ls::execute_with_context(args.iter().cloned(), ctx));
164 #[cfg(feature = "mkdir")]
165 reg.register("mkdir", |args, ctx| wasibox_core::utils::mkdir::execute_with_context(args.iter().cloned(), ctx));
166 #[cfg(feature = "mv")]
167 reg.register("mv", |args, ctx| wasibox_core::utils::mv::execute_with_context(args.iter().cloned(), ctx));
168 #[cfg(feature = "pwd")]
169 reg.register("pwd", |args, ctx| wasibox_core::utils::pwd::execute_with_context(args.iter().cloned(), ctx));
170 #[cfg(feature = "rm")]
171 reg.register("rm", |args, ctx| wasibox_core::utils::rm::execute_with_context(args.iter().cloned(), ctx));
172 #[cfg(feature = "rmdir")]
173 reg.register("rmdir", |args, ctx| wasibox_core::utils::rmdir::execute_with_context(args.iter().cloned(), ctx));
174 #[cfg(feature = "seq")]
175 reg.register("seq", |args, ctx| wasibox_core::utils::seq::execute_with_context(args.iter().cloned(), ctx));
176 #[cfg(feature = "sleep")]
177 reg.register("sleep", |args, ctx| wasibox_core::utils::sleep::execute_with_context(args.iter().cloned(), ctx));
178 #[cfg(feature = "tail")]
179 reg.register("tail", |args, ctx| wasibox_core::utils::tail::execute_with_context(args.iter().cloned(), ctx));
180 #[cfg(feature = "tee")]
181 reg.register("tee", |args, ctx| wasibox_core::utils::tee::execute_with_context(args.iter().cloned(), ctx));
182 #[cfg(feature = "touch")]
183 reg.register("touch", |args, ctx| wasibox_core::utils::touch::execute_with_context(args.iter().cloned(), ctx));
184 #[cfg(feature = "tree")]
185 reg.register("tree", |args, ctx| wasibox_core::utils::tree::execute_with_context(args.iter().cloned(), ctx));
186 #[cfg(feature = "true")]
187 reg.register("true", |args, ctx| wasibox_core::utils::r#true::execute_with_context(args.iter().cloned(), ctx));
188 #[cfg(feature = "uname")]
189 reg.register("uname", |args, ctx| wasibox_core::utils::uname::execute_with_context(args.iter().cloned(), ctx));
190 #[cfg(feature = "unlink")]
191 reg.register("unlink", |args, ctx| wasibox_core::utils::unlink::execute_with_context(args.iter().cloned(), ctx));
192 #[cfg(feature = "wc")]
193 reg.register("wc", |args, ctx| wasibox_core::utils::wc::execute_with_context(args.iter().cloned(), ctx));
194 #[cfg(feature = "whoami")]
195 reg.register("whoami", |args, ctx| wasibox_core::utils::whoami::execute_with_context(args.iter().cloned(), ctx));
196 #[cfg(feature = "yes")]
197 reg.register("yes", |args, ctx| wasibox_core::utils::yes::execute_with_context(args.iter().cloned(), ctx));
198
199 reg
200 }
201
202 pub fn register<F>(&mut self, name: impl Into<String>, handler: F)
204 where
205 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
206 {
207 self.commands.insert(name.into(), Arc::new(handler));
208 }
209
210 pub fn set_fallback<F>(&mut self, handler: F)
212 where
213 F: Fn(&[String], &mut IoContext) -> Result<(), String> + Send + Sync + 'static,
214 {
215 self.fallback = Some(Arc::new(handler));
216 }
217
218 pub fn remove_fallback(&mut self) {
220 self.fallback = None;
221 }
222
223 pub fn execute(&self, args: &[String], ctx: &mut IoContext) -> Result<(), String> {
225 if args.is_empty() {
226 return Ok(());
227 }
228 let cmd = &args[0];
229
230 if let Some(handler) = self.commands.get(cmd.as_str()) {
231 return handler(args, ctx);
232 }
233 if let Some(ref fallback) = self.fallback {
234 return fallback(args, ctx);
235 }
236 Err(format!("command not found: {}", cmd))
237 }
238}
239
240impl Default for CommandRegistry {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246fn is_broken_pipe(err: &str) -> bool {
252 err.contains("Broken pipe")
253 || err.contains("BrokenPipe")
254 || err.contains("broken pipe")
255}
256
257pub fn handle_pipeline(
262 line: &str,
263 initial_stdin: Box<dyn Read + Send + 'static>,
264 final_stdout: Box<dyn Write + Send + 'static>,
265 registry: &CommandRegistry,
266) -> Result<(), String> {
267 let stages_str: Vec<&str> = line.split('|').collect();
268
269 let mut final_stdout = Some(final_stdout);
270
271 std::thread::scope(|s| {
272 let mut prev_reader: Box<dyn Read + Send> = initial_stdin;
273 let mut threads = Vec::new();
274
275 for (i, stage_str) in stages_str.iter().enumerate() {
276 let is_last = i == stages_str.len() - 1;
277 let mut tokens = shlex::split(stage_str.trim())
278 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
279
280 let stdin = std::mem::replace(&mut prev_reader, Box::new(io::empty()));
281 let (stdout, next_reader): (Box<dyn Write + Send>, Option<Box<dyn Read + Send>>) = if is_last {
282 let mut out: Box<dyn Write + Send> = final_stdout.take().unwrap();
283 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
284 let append = tokens[pos] == ">>";
285 let filename = tokens.get(pos + 1).ok_or("Error: Missing file for redirection")?;
286 let file = if append {
287 std::fs::OpenOptions::new().create(true).append(true).open(filename)
288 } else {
289 File::create(filename)
290 }.map_err(|e| format!("Error opening file: {}", e))?;
291 out = Box::new(file);
292 tokens.truncate(pos);
293 }
294 (out, None)
295 } else {
296 let (reader, writer) = create_pipe();
297 (Box::new(writer), Some(Box::new(reader)))
298 };
299
300 if let Some(r) = next_reader {
301 prev_reader = r;
302 }
303
304 let thread = s.spawn(move || {
305 let mut ctx = IoContext::new(stdin, stdout, Box::new(io::stderr()));
306 registry.execute(&tokens, &mut ctx)
307 });
308 threads.push((i, thread));
309 }
310
311 let last_idx = stages_str.len() - 1;
314 let mut final_res = Ok(());
315 for (idx, thread) in threads {
316 match thread.join() {
317 Ok(Ok(())) => {}
318 Ok(Err(e)) => {
319 if idx != last_idx && is_broken_pipe(&e) {
320 continue;
321 }
322 if final_res.is_ok() {
323 final_res = Err(e);
324 }
325 }
326 Err(_) => {
327 if final_res.is_ok() {
328 final_res = Err("Thread panicked".to_string());
329 }
330 }
331 }
332 }
333 final_res
334 })
335}
336
337pub fn handle_parallel(
342 lines: Vec<String>,
343 initial_stdin: Box<dyn Read + Send + 'static>,
344 final_stdout: Box<dyn Write + Send + 'static>,
345 registry: Arc<CommandRegistry>,
346) -> Vec<Result<(), String>> {
347 let mut handles = Vec::new();
348 let mut stdin_opt: Option<Box<dyn Read + Send>> = Some(initial_stdin);
349 let mut stdout_opt: Option<Box<dyn Write + Send>> = Some(final_stdout);
350
351 for line in lines {
352 let registry = Arc::clone(®istry);
353 let stdin: Box<dyn Read + Send> = stdin_opt.take().unwrap_or_else(|| Box::new(io::empty()));
354 let stdout: Box<dyn Write + Send> = stdout_opt.take().unwrap_or_else(|| Box::new(io::stdout()));
355 let handle = std::thread::spawn(move || {
356 handle_pipeline(&line, stdin, stdout, &*registry)
357 });
358 handles.push(handle);
359 }
360
361 handles.into_iter()
362 .map(|h| h.join().unwrap_or(Err("Thread panicked".to_string())))
363 .collect()
364}
365
366struct PipeReader {
371 rx: std::sync::mpsc::Receiver<Vec<u8>>,
372 buffer: Vec<u8>,
373 pos: usize,
374}
375impl Read for PipeReader {
376 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
377 if self.pos >= self.buffer.len() {
378 match self.rx.recv() {
379 Ok(data) => {
380 self.buffer = data;
381 self.pos = 0;
382 }
383 Err(_) => return Ok(0), }
385 }
386 let available = self.buffer.len() - self.pos;
387 let to_copy = std::cmp::min(available, buf.len());
388 buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
389 self.pos += to_copy;
390 Ok(to_copy)
391 }
392}
393
394struct PipeWriter {
395 tx: std::sync::mpsc::Sender<Vec<u8>>,
396}
397impl Write for PipeWriter {
398 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
399 self.tx.send(buf.to_vec()).map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?;
400 Ok(buf.len())
401 }
402 fn flush(&mut self) -> io::Result<()> { Ok(()) }
403}
404
405fn create_pipe() -> (PipeReader, PipeWriter) {
406 let (tx, rx) = std::sync::mpsc::channel();
407 (PipeReader { rx, buffer: Vec::new(), pos: 0 }, PipeWriter { tx })
408}
409
410pub struct ArcVecWriter {
416 pub inner: Arc<Mutex<Vec<u8>>>,
417}
418impl Write for ArcVecWriter {
419 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
420 let mut inner = self.inner.lock().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
421 inner.extend_from_slice(buf);
422 Ok(buf.len())
423 }
424 fn flush(&mut self) -> io::Result<()> { Ok(()) }
425}
426
427#[cfg(test)]
432mod tests {
433 use super::*;
434 use std::io::{BufRead, BufReader, Cursor};
435
436 fn get_temp_dir() -> tempfile::TempDir {
437 #[cfg(target_os = "wasi")]
438 {
439 tempfile::Builder::new()
440 .prefix("test_")
441 .tempdir_in(".")
442 .expect("Failed to create temp dir in current directory")
443 }
444 #[cfg(not(target_os = "wasi"))]
445 {
446 tempfile::tempdir().expect("Failed to create system temp dir")
447 }
448 }
449
450 fn builtins() -> CommandRegistry {
452 CommandRegistry::with_builtins()
453 }
454
455 #[test]
458 fn test_simple_echo() {
459 let out = Arc::new(Mutex::new(Vec::new()));
460 handle_pipeline("echo hello", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
461 let buf = out.lock().unwrap();
462 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
463 }
464
465 #[test]
466 fn test_pipe_echo_cat() {
467 let out = Arc::new(Mutex::new(Vec::new()));
468 handle_pipeline("echo hello | cat", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
469 let buf = out.lock().unwrap();
470 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
471 }
472
473 #[test]
474 fn test_pipe_grep() {
475 let out = Arc::new(Mutex::new(Vec::new()));
476 handle_pipeline("echo world | grep world", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
477 let buf = out.lock().unwrap();
478 assert_eq!(String::from_utf8_lossy(&buf).trim(), "world");
479 }
480
481 #[test]
482 fn test_redirection_create() {
483 let dir = get_temp_dir();
484 let file_path = dir.path().join("test_create.txt");
485 let cmd = format!("echo hello > \"{}\"", file_path.display());
486 handle_pipeline(&cmd, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins()).unwrap();
487 let content = std::fs::read_to_string(file_path).unwrap();
488 assert_eq!(content.trim(), "hello");
489 }
490
491 #[test]
492 fn test_redirection_append() {
493 let dir = get_temp_dir();
494 let file_path = dir.path().join("test_append.txt");
495 let cmd1 = format!("echo hello > \"{}\"", file_path.display());
496 handle_pipeline(&cmd1, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins()).unwrap();
497 let cmd2 = format!("echo world >> \"{}\"", file_path.display());
498 handle_pipeline(&cmd2, Box::new(Cursor::new("")), Box::new(io::sink()), &builtins()).unwrap();
499 let content = std::fs::read_to_string(file_path).unwrap();
500 assert!(content.contains("hello"));
501 assert!(content.contains("world"));
502 }
503
504 #[test]
505 fn test_complex_pipeline() {
506 let out = Arc::new(Mutex::new(Vec::new()));
507 handle_pipeline("echo hello | grep h | wc -c", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
508 let buf = out.lock().unwrap();
509 assert_eq!(String::from_utf8_lossy(&buf).trim(), "6");
510 }
511
512 #[test]
515 fn test_custom_command() {
516 let mut reg = CommandRegistry::with_builtins();
517 reg.register("magic", |_args, ctx| {
518 write!(ctx.stdout, "magic happen").map_err(|e| e.to_string())
519 });
520
521 let out = Arc::new(Mutex::new(Vec::new()));
522 handle_pipeline("magic", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
523 let buf = out.lock().unwrap();
524 assert_eq!(String::from_utf8_lossy(&buf), "magic happen");
525 }
526
527 #[test]
528 fn test_custom_command_in_pipeline() {
529 let mut reg = CommandRegistry::with_builtins();
531 reg.register("double", |_args, ctx| {
532 for line in BufReader::new(&mut ctx.stdin).lines() {
533 let line = match line {
534 Ok(l) => l,
535 Err(_) => break,
536 };
537 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
538 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
539 }
540 Ok(())
541 });
542
543 let out = Arc::new(Mutex::new(Vec::new()));
544 handle_pipeline("echo hello | double", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
545 let buf = out.lock().unwrap();
546 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello\nhello");
547 }
548
549 #[test]
550 fn test_override_builtin() {
551 let mut reg = CommandRegistry::with_builtins();
553 reg.register("echo", |args, ctx| {
554 let msg = args[1..].join(" ").to_uppercase();
555 writeln!(ctx.stdout, "{}", msg).map_err(|e| e.to_string())
556 });
557
558 let out = Arc::new(Mutex::new(Vec::new()));
559 handle_pipeline("echo hello", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
560 let buf = out.lock().unwrap();
561 assert_eq!(String::from_utf8_lossy(&buf).trim(), "HELLO");
562 }
563
564 #[test]
567 fn test_all_external_pipeline() {
568 let mut reg = CommandRegistry::new();
569 reg.remove_fallback(); reg.register("count", |_args, ctx| {
573 for i in 1u64.. {
574 if writeln!(ctx.stdout, "{}", i).is_err() { break; }
575 }
576 Ok(())
577 });
578
579 reg.register("filter2", |_args, ctx| {
581 for line in BufReader::new(&mut ctx.stdin).lines() {
582 let line = match line {
583 Ok(l) => l,
584 Err(_) => break,
585 };
586 if line.contains('2') {
587 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
588 }
589 }
590 Ok(())
591 });
592
593 reg.register("take", |args, ctx| {
595 let n: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5);
596 for (i, line) in BufReader::new(&mut ctx.stdin).lines().enumerate() {
597 if i >= n { break; }
598 let line = match line {
599 Ok(l) => l,
600 Err(_) => break,
601 };
602 if writeln!(ctx.stdout, "{}", line).is_err() { break; }
603 }
604 Ok(())
605 });
606
607 let out = Arc::new(Mutex::new(Vec::new()));
608 handle_pipeline(
609 "count | filter2 | take 3",
610 Box::new(io::empty()),
611 Box::new(ArcVecWriter { inner: Arc::clone(&out) }),
612 ®,
613 ).unwrap();
614
615 let buf = out.lock().unwrap();
616 let result = String::from_utf8_lossy(&buf);
617 let lines: Vec<&str> = result.trim().lines().collect();
618 assert_eq!(lines.len(), 3);
619 for line in &lines {
620 assert!(line.contains('2'), "expected '2' in line, got: {}", line);
621 }
622 assert_eq!(lines[0], "2");
623 assert_eq!(lines[1], "12");
624 assert_eq!(lines[2], "20");
625 }
626
627 #[test]
630 fn test_parallel_execution() {
631 let mut reg = CommandRegistry::with_builtins();
632 reg.register("slow", |_args, _ctx| {
633 std::thread::sleep(std::time::Duration::from_millis(100));
634 Ok(())
635 });
636 let registry = Arc::new(reg);
637
638 let lines = vec!["slow".to_string(), "slow".to_string(), "slow".to_string()];
639 let start = std::time::Instant::now();
640 let results = handle_parallel(lines, Box::new(io::empty()), Box::new(io::sink()), registry);
641 let duration = start.elapsed();
642
643 assert_eq!(results.len(), 3);
644 for res in results {
645 assert!(res.is_ok());
646 }
647 assert!(duration < std::time::Duration::from_millis(250));
648 }
649
650 #[test]
653 fn test_streaming_pipeline() {
654 let out = Arc::new(Mutex::new(Vec::new()));
655 handle_pipeline("yes | head -n 2", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
656 let buf = out.lock().unwrap();
657 assert_eq!(String::from_utf8_lossy(&buf).trim(), "y\ny");
658 }
659
660 #[test]
661 fn test_seq_pipeline_head() {
662 let out = Arc::new(Mutex::new(Vec::new()));
663 handle_pipeline("seq | head -n 5", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
664 let buf = out.lock().unwrap();
665 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3\n4\n5");
666 }
667
668 #[test]
669 fn test_seq_pipeline_grep_head() {
670 let out = Arc::new(Mutex::new(Vec::new()));
671 handle_pipeline("seq | grep 2 | head -n 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
672 let buf = out.lock().unwrap();
673 let result = String::from_utf8_lossy(&buf);
674 let lines: Vec<&str> = result.trim().lines().collect();
675 assert_eq!(lines.len(), 3);
676 for line in &lines {
677 assert!(line.contains('2'));
678 }
679 assert_eq!(lines[0], "2");
680 assert_eq!(lines[1], "12");
681 assert_eq!(lines[2], "20");
682 }
683
684 #[test]
685 fn test_seq_finite() {
686 let out = Arc::new(Mutex::new(Vec::new()));
687 handle_pipeline("seq 3", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
688 let buf = out.lock().unwrap();
689 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n2\n3");
690 }
691
692 #[test]
693 fn test_seq_range() {
694 let out = Arc::new(Mutex::new(Vec::new()));
695 handle_pipeline("seq 5 8", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
696 let buf = out.lock().unwrap();
697 assert_eq!(String::from_utf8_lossy(&buf).trim(), "5\n6\n7\n8");
698 }
699
700 #[test]
701 fn test_seq_step() {
702 let out = Arc::new(Mutex::new(Vec::new()));
703 handle_pipeline("seq 1 2 10", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), &builtins()).unwrap();
704 let buf = out.lock().unwrap();
705 assert_eq!(String::from_utf8_lossy(&buf).trim(), "1\n3\n5\n7\n9");
706 }
707 #[test]
708 fn test_cd_and_ls() {
709 let dir = get_temp_dir();
710 std::fs::write(dir.path().join("aaa.txt"), "hello").unwrap();
712 std::fs::write(dir.path().join("bbb.txt"), "world").unwrap();
713
714 let original_cwd = env::current_dir().unwrap();
715 let reg = builtins();
716
717 let cd_cmd = format!("cd \"{}\"", dir.path().display());
719 handle_pipeline(&cd_cmd, Box::new(io::empty()), Box::new(io::sink()), ®).unwrap();
720
721 let out = Arc::new(Mutex::new(Vec::new()));
723 handle_pipeline("ls", Box::new(io::empty()), Box::new(ArcVecWriter { inner: Arc::clone(&out) }), ®).unwrap();
724
725 let buf = out.lock().unwrap();
726 let output = String::from_utf8_lossy(&buf);
727 assert!(output.contains("aaa.txt"), "expected aaa.txt in ls output, got: {}", output);
728 assert!(output.contains("bbb.txt"), "expected bbb.txt in ls output, got: {}", output);
729
730 env::set_current_dir(original_cwd).unwrap();
732 }
733}