rush_sh/
builtins.rs

1use std::io::{self, Write};
2use std::os::unix::io::FromRawFd;
3
4use crate::parser::ShellCommand;
5use crate::state::ShellState;
6
7/// A writer wrapper for output handling
8pub struct ColoredWriter<W: Write> {
9    inner: W,
10}
11
12impl<W: Write> ColoredWriter<W> {
13    pub fn new(inner: W) -> Self {
14        Self { inner }
15    }
16}
17
18impl<W: Write> Write for ColoredWriter<W> {
19    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
20        self.inner.write(buf)
21    }
22
23    fn flush(&mut self) -> io::Result<()> {
24        self.inner.flush()
25    }
26}
27
28/// A writer that always returns EBADF
29pub struct BadFdWriter;
30
31impl Write for BadFdWriter {
32    fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
33        Err(io::Error::from_raw_os_error(libc::EBADF))
34    }
35
36    fn flush(&mut self) -> io::Result<()> {
37        Err(io::Error::from_raw_os_error(libc::EBADF))
38    }
39}
40
41mod builtin_alias;
42mod builtin_break;
43mod builtin_cd;
44mod builtin_continue;
45mod builtin_declare;
46mod builtin_dirs;
47mod builtin_env;
48mod builtin_exit;
49mod builtin_export;
50mod builtin_help;
51mod builtin_popd;
52mod builtin_pushd;
53mod builtin_pwd;
54mod builtin_return;
55mod builtin_set;
56mod builtin_set_color_scheme;
57mod builtin_set_colors;
58mod builtin_set_condensed;
59mod builtin_shift;
60mod builtin_source;
61mod builtin_test;
62mod builtin_trap;
63mod builtin_type;
64mod builtin_unalias;
65mod builtin_unset;
66
67pub trait Builtin {
68    fn name(&self) -> &'static str;
69    fn names(&self) -> Vec<&'static str>;
70    fn description(&self) -> &'static str;
71    fn run(
72        &self,
73        cmd: &ShellCommand,
74        shell_state: &mut ShellState,
75        output_writer: &mut dyn Write,
76    ) -> i32;
77}
78
79/// Provides a vector of all builtin command implementations in registration order.
80///
81/// Each element is a boxed implementation of `Builtin` representing one builtin command
82/// available to the shell.
83///
84/// # Examples
85///
86/// ```
87/// // Note: get_builtins is a private function
88/// // Use is_builtin() or get_builtin_commands() instead for public API
89/// use rush_sh::builtins::is_builtin;
90/// assert!(is_builtin("cd"));
91/// assert!(is_builtin("pwd"));
92/// ```
93fn get_builtins() -> Vec<Box<dyn Builtin>> {
94    vec![
95        Box::new(builtin_cd::CdBuiltin),
96        Box::new(builtin_pwd::PwdBuiltin),
97        Box::new(builtin_env::EnvBuiltin),
98        Box::new(builtin_exit::ExitBuiltin),
99        Box::new(builtin_help::HelpBuiltin),
100        Box::new(builtin_source::SourceBuiltin),
101        Box::new(builtin_export::ExportBuiltin),
102        Box::new(builtin_unset::UnsetBuiltin),
103        Box::new(builtin_pushd::PushdBuiltin),
104        Box::new(builtin_popd::PopdBuiltin),
105        Box::new(builtin_dirs::DirsBuiltin),
106        Box::new(builtin_alias::AliasBuiltin),
107        Box::new(builtin_unalias::UnaliasBuiltin),
108        Box::new(builtin_test::TestBuiltin),
109        Box::new(builtin_set::SetBuiltin),
110        Box::new(builtin_set_colors::SetColorsBuiltin),
111        Box::new(builtin_set_color_scheme::SetColorSchemeBuiltin),
112        Box::new(builtin_set_condensed::SetCondensedBuiltin),
113        Box::new(builtin_shift::ShiftBuiltin),
114        Box::new(builtin_declare::DeclareBuiltin),
115        Box::new(builtin_trap::TrapBuiltin),
116        Box::new(builtin_type::TypeBuiltin),
117        Box::new(builtin_return::ReturnBuiltin),
118        Box::new(builtin_break::BreakBuiltin),
119        Box::new(builtin_continue::ContinueBuiltin),
120    ]
121}
122
123pub fn is_builtin(cmd: &str) -> bool {
124    get_builtins().iter().any(|b| b.names().contains(&cmd))
125}
126
127pub fn get_builtin_commands() -> Vec<String> {
128    let builtins = get_builtins();
129    let mut commands = Vec::new();
130    for b in builtins {
131        for &name in &b.names() {
132            commands.push(name.to_string());
133        }
134    }
135    commands
136}
137
138/// Execute a builtin command, applying redirections and selecting the appropriate output writer.
139///
140/// This function locates and runs the builtin named by `cmd.args[0]`, applying any redirections
141/// from `cmd.redirections` in left-to-right order, expanding filenames using `shell_state`,
142/// saving and restoring file descriptors around the builtin invocation, and selecting stdout
143/// from the shell's file-descriptor table (or using a sink writer if stdout is closed).
144/// If `output_override` is provided, it is used directly as the builtin's output writer and
145/// redirections are not applied. Colored error messages are printed according to `shell_state`'s
146/// color settings. On success it returns the builtin's exit code; on failure it returns `1`.
147///
148/// # Examples
149///
150/// ```no_run
151/// use rush_sh::builtins::execute_builtin;
152/// use rush_sh::parser::ShellCommand;
153/// use rush_sh::ShellState;
154/// // Construct a ShellCommand and ShellState appropriately in real code.
155/// let cmd = ShellCommand { args: vec!["pwd".into()], redirections: vec![], compound: None };
156/// let mut state = ShellState::new();
157/// let exit_code = execute_builtin(&cmd, &mut state, None);
158/// println!("exit code: {}", exit_code);
159/// ```
160pub fn execute_builtin(
161    cmd: &ShellCommand,
162    shell_state: &mut ShellState,
163    output_override: Option<Box<dyn Write>>,
164) -> i32 {
165    // Helper function for colored error messages
166    let colors_enabled = shell_state.colors_enabled;
167    let error_color = shell_state.color_scheme.error.clone();
168    let print_error = move |msg: &str| {
169        if colors_enabled {
170            eprintln!("{}{}\x1b[0m", error_color, msg);
171        } else {
172            eprintln!("{}", msg);
173        }
174    };
175
176    // If output_override is provided, use the old simple path for command substitution
177    if let Some(mut output_writer) = output_override {
178        let builtins = get_builtins();
179        if let Some(builtin) = builtins
180            .into_iter()
181            .find(|b| b.names().contains(&cmd.args[0].as_str()))
182        {
183            return builtin.run(cmd, shell_state, &mut *output_writer);
184        } else {
185            return 1;
186        }
187    }
188
189    // Handle redirections using FileDescriptorTable for proper POSIX compliance
190    use crate::parser::Redirection;
191
192    // Clone redirections to avoid borrow checker issues
193    let redirections = cmd.redirections.clone();
194
195    // First, expand all filenames in redirections (needs mutable borrow of shell_state)
196    // Collect all filenames that need expansion
197    let mut files_to_expand: Vec<String> = Vec::new();
198    for redir in &redirections {
199        match redir {
200            Redirection::Input(file)
201            | Redirection::Output(file)
202            | Redirection::OutputClobber(file)
203            | Redirection::Append(file)
204            | Redirection::FdInput(_, file)
205            | Redirection::FdOutput(_, file)
206            | Redirection::FdOutputClobber(_, file)
207            | Redirection::FdAppend(_, file)
208            | Redirection::FdInputOutput(_, file) => {
209                files_to_expand.push(file.clone());
210            }
211            _ => {
212                files_to_expand.push(String::new()); // Placeholder for non-file redirections
213            }
214        }
215    }
216
217    // Now expand all filenames (single mutable borrow)
218    let mut expanded_files: Vec<String> = Vec::new();
219    for f in &files_to_expand {
220        if f.is_empty() {
221            expanded_files.push(String::new());
222        } else {
223            expanded_files.push(crate::executor::expand_variables_in_string(f, shell_state));
224        }
225    }
226
227    // Pair redirections with their expanded filenames
228    let mut expanded_redirections: Vec<(Redirection, Option<String>)> = Vec::new();
229    for (i, redir) in redirections.iter().enumerate() {
230        let expanded_file = if expanded_files[i].is_empty() {
231            None
232        } else {
233            Some(expanded_files[i].clone())
234        };
235        expanded_redirections.push((redir.clone(), expanded_file));
236    }
237
238    // Save all current file descriptors before applying redirections
239    if let Err(e) = shell_state.fd_table.borrow_mut().save_all_fds() {
240        print_error(&format!("Failed to save file descriptors: {}", e));
241        return 1;
242    }
243
244    // Apply all redirections in left-to-right order (POSIX requirement)
245    for (redir, expanded_file) in &expanded_redirections {
246        let result = match redir {
247            Redirection::Input(_) => {
248                let file = expanded_file.as_ref().unwrap();
249                shell_state.fd_table.borrow_mut().open_fd(
250                    0, file, true,  // read
251                    false, // write
252                    false, // append
253                    false, // truncate
254                    false, // create_new
255                )
256            }
257            Redirection::Output(_) | Redirection::OutputClobber(_) => {
258                let file = expanded_file.as_ref().unwrap();
259                shell_state.fd_table.borrow_mut().open_fd(
260                    1, file, false, // read
261                    true,  // write
262                    false, // append
263                    true,  // truncate
264                    false, // create_new
265                )
266            }
267            Redirection::Append(_) => {
268                let file = expanded_file.as_ref().unwrap();
269                shell_state.fd_table.borrow_mut().open_fd(
270                    1, file, false, // read
271                    true,  // write
272                    true,  // append
273                    false, // truncate
274                    false, // create_new
275                )
276            }
277            Redirection::FdInput(fd, _) => {
278                let file = expanded_file.as_ref().unwrap();
279                shell_state.fd_table.borrow_mut().open_fd(
280                    *fd, file, true,  // read
281                    false, // write
282                    false, // append
283                    false, // truncate
284                    false, // create_new
285                )
286            }
287            Redirection::FdOutput(fd, _) | Redirection::FdOutputClobber(fd, _) => {
288                let file = expanded_file.as_ref().unwrap();
289                shell_state.fd_table.borrow_mut().open_fd(
290                    *fd, file, false, // read
291                    true,  // write
292                    false, // append
293                    true,  // truncate
294                    false, // create_new
295                )
296            }
297            Redirection::FdAppend(fd, _) => {
298                let file = expanded_file.as_ref().unwrap();
299                shell_state.fd_table.borrow_mut().open_fd(
300                    *fd, file, false, // read
301                    true,  // write
302                    true,  // append
303                    false, // truncate
304                    false, // create_new
305                )
306            }
307            Redirection::FdDuplicate(target_fd, source_fd) => shell_state
308                .fd_table
309                .borrow_mut()
310                .duplicate_fd(*source_fd, *target_fd),
311            Redirection::FdClose(fd) => shell_state.fd_table.borrow_mut().close_fd(*fd),
312            Redirection::FdInputOutput(fd, _) => {
313                let file = expanded_file.as_ref().unwrap();
314                shell_state.fd_table.borrow_mut().open_fd(
315                    *fd, file, true,  // read
316                    true,  // write
317                    false, // append
318                    false, // truncate
319                    false, // create_new
320                )
321            }
322            // Here-documents and here-strings are handled differently for builtins
323            // They don't modify the fd table directly
324            Redirection::HereDoc(_, _) | Redirection::HereString(_) => Ok(()),
325        };
326
327        if let Err(e) = result {
328            print_error(&format!("Redirection error: {}", e));
329            // Restore file descriptors before returning
330            let _ = shell_state.fd_table.borrow_mut().restore_all_fds();
331            return 1;
332        }
333    }
334
335    // Get output writer - try to get FD 1 from fd_table to respect redirections
336    let mut output_writer: Box<dyn Write> = {
337        let raw_fd = shell_state.fd_table.borrow().get_raw_fd(1);
338        match raw_fd {
339            Some(fd) => {
340                // Duplicate the fd so we can take ownership in a File
341                // (using unsafe libc call similar to how state.rs handles it)
342                let dup_fd = unsafe { libc::dup(fd) };
343                if dup_fd >= 0 {
344                    let file = unsafe { std::fs::File::from_raw_fd(dup_fd) };
345                    Box::new(ColoredWriter::new(file))
346                } else {
347                    // Duplication failed
348                    let err = io::Error::last_os_error();
349                    if err.raw_os_error() == Some(libc::EBADF) {
350                        // EBADF means the FD is closed/invalid (e.g. parent closed stdout).
351                        // In this case, we just run without output.
352                        Box::new(BadFdWriter)
353                    } else {
354                        // Other errors (e.g. EMFILE) are fatal
355                        print_error(&format!("Failed to duplicate stdout: {}", err));
356                        let _ = shell_state.fd_table.borrow_mut().restore_all_fds();
357                        return 1;
358                    }
359                }
360            }
361            None => {
362                // FD 1 is closed. Do NOT fall back to stdout.
363                Box::new(BadFdWriter)
364            }
365        }
366    };
367
368    // Execute the builtin command
369    let builtins = get_builtins();
370    let exit_code = if let Some(builtin) = builtins
371        .into_iter()
372        .find(|b| b.names().contains(&cmd.args[0].as_str()))
373    {
374        builtin.run(cmd, shell_state, &mut *output_writer)
375    } else {
376        1
377    };
378
379    // Restore all file descriptors after builtin execution
380    if let Err(e) = shell_state.fd_table.borrow_mut().restore_all_fds() {
381        print_error(&format!("Failed to restore file descriptors: {}", e));
382        return 1;
383    }
384
385    exit_code
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_is_builtin() {
394        assert!(is_builtin("cd"));
395        assert!(is_builtin("pwd"));
396        assert!(is_builtin("env"));
397        assert!(is_builtin("exit"));
398        assert!(is_builtin("help"));
399        assert!(is_builtin("alias"));
400        assert!(is_builtin("unalias"));
401        assert!(is_builtin("test"));
402        assert!(is_builtin("["));
403        assert!(is_builtin("."));
404        assert!(!is_builtin("ls"));
405        assert!(!is_builtin("grep"));
406        assert!(!is_builtin("echo"));
407    }
408
409    #[test]
410    fn test_execute_builtin_unknown() {
411        let cmd = ShellCommand {
412            args: vec!["unknown".to_string()],
413            redirections: Vec::new(),
414            compound: None,
415        };
416        let mut shell_state = ShellState::new();
417        let exit_code = execute_builtin(&cmd, &mut shell_state, None);
418        assert_eq!(exit_code, 1);
419    }
420
421    #[test]
422    fn test_get_builtin_commands() {
423        let commands = get_builtin_commands();
424        assert!(commands.contains(&"cd".to_string()));
425        assert!(commands.contains(&"pwd".to_string()));
426        assert!(commands.contains(&"env".to_string()));
427        assert!(commands.contains(&"exit".to_string()));
428        assert!(commands.contains(&"help".to_string()));
429        assert!(commands.contains(&"source".to_string()));
430        assert!(commands.contains(&"export".to_string()));
431        assert!(commands.contains(&"unset".to_string()));
432        assert!(commands.contains(&"pushd".to_string()));
433        assert!(commands.contains(&"popd".to_string()));
434        assert!(commands.contains(&"dirs".to_string()));
435        assert!(commands.contains(&"alias".to_string()));
436        assert!(commands.contains(&"unalias".to_string()));
437        assert!(commands.contains(&"test".to_string()));
438        assert!(commands.contains(&"[".to_string()));
439        assert!(commands.contains(&".".to_string()));
440        assert!(commands.contains(&"set_colors".to_string()));
441        assert!(commands.contains(&"set_color_scheme".to_string()));
442        assert!(commands.contains(&"set_condensed".to_string()));
443        assert!(commands.contains(&"return".to_string()));
444        assert!(commands.contains(&"break".to_string()));
445        assert!(commands.contains(&"continue".to_string()));
446        assert!(commands.contains(&"set".to_string()));
447        assert_eq!(commands.len(), 27);
448    }
449}