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