Skip to main content

wasi_shell/
lib.rs

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            // Retrieve the data from the Arc
86            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            // Avoid any logic that might indirectly call env::temp_dir()
145            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        // Our 'echo' likely doesn't support -e. Let's use a simpler check.
176        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}