1use std::env;
2use std::fs::File;
3use std::io::{self, Cursor, Read, Write};
4use std::sync::{Arc, Mutex};
5use colored::*;
6pub use wasibox_core::IoContext;
7
8pub fn handle_pipeline<R, W>(line: &str, initial_stdin: Box<R>, final_stdout: Box<W>) -> Result<(), String>
9where
10 R: Read + Send + 'static,
11 W: Write + Send + 'static,
12{
13 let stages: Vec<&str> = line.split('|').collect();
14
15 if stages.len() == 1 {
16 let mut tokens = shlex::split(stages[0].trim())
17 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
18
19 let mut stdout: Box<dyn Write + Send> = final_stdout;
20 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
21 let append = tokens[pos] == ">>";
22 let filename = tokens.get(pos + 1).ok_or("Error: Missing file for redirection")?;
23 let file = if append {
24 std::fs::OpenOptions::new().create(true).append(true).open(filename)
25 } else {
26 File::create(filename)
27 }.map_err(|e| format!("Error opening file: {}", e))?;
28 stdout = Box::new(file);
29 tokens.truncate(pos);
30 }
31
32 let mut ctx = IoContext::new(
33 initial_stdin,
34 stdout,
35 Box::new(io::stderr()),
36 );
37 return execute_command(tokens, &mut ctx);
38 }
39
40 let mut last_output = Vec::new();
41 let mut initial_stdin = Some(initial_stdin);
42 let mut final_stdout = Some(final_stdout);
43
44 for (i, stage) in stages.iter().enumerate() {
45 let is_first = i == 0;
46 let is_last = i == stages.len() - 1;
47
48 let mut tokens = shlex::split(stage.trim())
49 .ok_or_else(|| "Error: Invalid input (quoting error)".to_string())?;
50
51 let stdin: Box<dyn Read + Send> = if is_first {
52 initial_stdin.take().ok_or("Internal error: initial_stdin missing")?
53 } else {
54 Box::new(Cursor::new(last_output.clone()))
55 };
56
57 let pipe_buf = Arc::new(Mutex::new(Vec::new()));
58 let stdout: Box<dyn Write + Send> = if is_last {
59 if let Some(pos) = tokens.iter().position(|t| t == ">" || t == ">>") {
60 let append = tokens[pos] == ">>";
61 let filename = tokens.get(pos + 1).ok_or("Error: Missing file for redirection")?;
62 let file = if append {
63 std::fs::OpenOptions::new().create(true).append(true).open(filename)
64 } else {
65 File::create(filename)
66 }.map_err(|e| format!("Error opening file: {}", e))?;
67 tokens.truncate(pos);
68 Box::new(file)
69 } else {
70 final_stdout.take().ok_or("Internal error: final_stdout missing")?
71 }
72 } else {
73 Box::new(ArcVecWriter { inner: Arc::clone(&pipe_buf) })
74 };
75
76 let mut ctx = IoContext::new(
77 stdin,
78 stdout,
79 Box::new(io::stderr()),
80 );
81
82 execute_command(tokens, &mut ctx)?;
83
84 if !is_last {
85 let buf = pipe_buf.lock().map_err(|_| "Internal error: pipe lock poisoned")?;
87 last_output = buf.clone();
88 }
89 }
90
91 Ok(())
92}
93
94struct ArcVecWriter {
95 inner: Arc<Mutex<Vec<u8>>>,
96}
97impl Write for ArcVecWriter {
98 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
99 let mut inner = self.inner.lock().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
100 inner.write(buf)
101 }
102 fn flush(&mut self) -> io::Result<()> {
103 let mut inner = self.inner.lock().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
104 inner.flush()
105 }
106}
107
108pub fn execute_command(args: Vec<String>, ctx: &mut IoContext) -> Result<(), String> {
109 if args.is_empty() {
110 return Ok(());
111 }
112
113 let cmd = &args[0];
114
115 match cmd.as_str() {
116 "help" => {
117 writeln!(ctx.stdout, "{}", "Available Commands:".yellow().bold()).map_err(|e| e.to_string())?;
118 writeln!(ctx.stdout, " Shell Built-ins: cd, help, exit").map_err(|e| e.to_string())?;
119 writeln!(ctx.stdout, " Animations: sl").map_err(|e| e.to_string())?;
120 writeln!(ctx.stdout, " Core Utilities: arch, cat, cp, dir, echo, grep, head, ls, mkdir, mv, pwd, rm, rmdir, sleep, tail, tee, touch, tree, uname, wc, whoami, yes, etc.").map_err(|e| e.to_string())?;
121 Ok(())
122 }
123 "cd" => {
124 let new_dir = args.get(1).map(|s| s.as_str()).unwrap_or(".");
125 env::set_current_dir(new_dir).map_err(|e| format!("cd: {}", e))
126 }
127 "exit" => Ok(()),
128 "sl" => {
129 let _ = sl::run(args.iter().cloned());
130 Ok(())
131 }
132 _ => wasibox_core::execute_with_context(args.iter().cloned(), ctx),
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::io::Cursor;
140
141 fn get_temp_dir() -> tempfile::TempDir {
142 #[cfg(target_os = "wasi")]
143 {
144 tempfile::Builder::new()
146 .prefix("test_")
147 .tempdir_in(".")
148 .expect("Failed to create temp dir in current directory")
149 }
150 #[cfg(not(target_os = "wasi"))]
151 {
152 tempfile::tempdir().expect("Failed to create system temp dir")
153 }
154 }
155
156 #[test]
157 fn test_simple_echo() {
158 let out = Arc::new(Mutex::new(Vec::new()));
159 handle_pipeline("echo hello", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) })).unwrap();
160 let buf = out.lock().unwrap();
161 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
162 }
163
164 #[test]
165 fn test_pipe_echo_cat() {
166 let out = Arc::new(Mutex::new(Vec::new()));
167 handle_pipeline("echo hello | cat", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) })).unwrap();
168 let buf = out.lock().unwrap();
169 assert_eq!(String::from_utf8_lossy(&buf).trim(), "hello");
170 }
171
172 #[test]
173 fn test_pipe_grep() {
174 let out = Arc::new(Mutex::new(Vec::new()));
175 handle_pipeline("echo world | grep world", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) })).unwrap();
177 let buf = out.lock().unwrap();
178 assert_eq!(String::from_utf8_lossy(&buf).trim(), "world");
179 }
180
181 #[test]
182 fn test_redirection_create() {
183 let dir = get_temp_dir();
184 let file_path = dir.path().join("test_create.txt");
185 let cmd = format!("echo hello > \"{}\"", file_path.display());
186
187 handle_pipeline(&cmd, Box::new(Cursor::new("")), Box::new(io::sink())).unwrap();
188
189 let content = std::fs::read_to_string(file_path).unwrap();
190 assert_eq!(content.trim(), "hello");
191 }
192
193 #[test]
194 fn test_redirection_append() {
195 let dir = get_temp_dir();
196 let file_path = dir.path().join("test_append.txt");
197
198 let cmd1 = format!("echo hello > \"{}\"", file_path.display());
199 handle_pipeline(&cmd1, Box::new(Cursor::new("")), Box::new(io::sink())).unwrap();
200
201 let cmd2 = format!("echo world >> \"{}\"", file_path.display());
202 handle_pipeline(&cmd2, Box::new(Cursor::new("")), Box::new(io::sink())).unwrap();
203
204 let content = std::fs::read_to_string(file_path).unwrap();
205 assert!(content.contains("hello"));
206 assert!(content.contains("world"));
207 }
208
209 #[test]
210 fn test_complex_pipeline() {
211 let out = Arc::new(Mutex::new(Vec::new()));
212 handle_pipeline("echo hello | grep h | wc -c", Box::new(Cursor::new("")), Box::new(ArcVecWriter { inner: Arc::clone(&out) })).unwrap();
213 let buf = out.lock().unwrap();
214 let result = String::from_utf8_lossy(&buf).trim().to_string();
215 assert_eq!(result, "6");
216 }
217}