rush_sh/
builtins.rs

1use std::fs::File;
2use std::io::{self, Write};
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
28mod builtin_alias;
29mod builtin_cd;
30mod builtin_dirs;
31mod builtin_env;
32mod builtin_exit;
33mod builtin_export;
34mod builtin_help;
35mod builtin_popd;
36mod builtin_pushd;
37mod builtin_pwd;
38mod builtin_set_color_scheme;
39mod builtin_set_colors;
40mod builtin_set_condensed;
41mod builtin_shift;
42mod builtin_source;
43mod builtin_test;
44mod builtin_unalias;
45mod builtin_unset;
46mod builtin_declare;
47
48pub trait Builtin {
49    fn name(&self) -> &'static str;
50    fn names(&self) -> Vec<&'static str>;
51    fn description(&self) -> &'static str;
52    fn run(
53        &self,
54        cmd: &ShellCommand,
55        shell_state: &mut ShellState,
56        output_writer: &mut dyn Write,
57    ) -> i32;
58}
59
60fn get_builtins() -> Vec<Box<dyn Builtin>> {
61    vec![
62        Box::new(builtin_cd::CdBuiltin),
63        Box::new(builtin_pwd::PwdBuiltin),
64        Box::new(builtin_env::EnvBuiltin),
65        Box::new(builtin_exit::ExitBuiltin),
66        Box::new(builtin_help::HelpBuiltin),
67        Box::new(builtin_source::SourceBuiltin),
68        Box::new(builtin_export::ExportBuiltin),
69        Box::new(builtin_unset::UnsetBuiltin),
70        Box::new(builtin_pushd::PushdBuiltin),
71        Box::new(builtin_popd::PopdBuiltin),
72        Box::new(builtin_dirs::DirsBuiltin),
73        Box::new(builtin_alias::AliasBuiltin),
74        Box::new(builtin_unalias::UnaliasBuiltin),
75        Box::new(builtin_test::TestBuiltin),
76        Box::new(builtin_set_colors::SetColorsBuiltin),
77        Box::new(builtin_set_color_scheme::SetColorSchemeBuiltin),
78        Box::new(builtin_set_condensed::SetCondensedBuiltin),
79        Box::new(builtin_shift::ShiftBuiltin),
80        Box::new(builtin_declare::DeclareBuiltin),
81    ]
82}
83
84pub fn is_builtin(cmd: &str) -> bool {
85    get_builtins().iter().any(|b| b.names().contains(&cmd))
86}
87
88pub fn get_builtin_commands() -> Vec<String> {
89    let builtins = get_builtins();
90    let mut commands = Vec::new();
91    for b in builtins {
92        for &name in &b.names() {
93            commands.push(name.to_string());
94        }
95    }
96    commands
97}
98
99pub fn execute_builtin(
100    cmd: &ShellCommand,
101    shell_state: &mut ShellState,
102    output_override: Option<Box<dyn std::io::Write>>,
103) -> i32 {
104    // Helper function for colored error messages
105    let print_error = |msg: &str| {
106        if shell_state.colors_enabled {
107            eprintln!("{}{}\x1b[0m", shell_state.color_scheme.error, msg);
108        } else {
109            eprintln!("{}", msg);
110        }
111    };
112    // Handle input redirection for built-ins that might need it
113    let _input_content = if let Some(ref input_file) = cmd.input {
114        match std::fs::read_to_string(input_file) {
115            Ok(content) => Some(content),
116            Err(e) => {
117                print_error(&format!("Error reading input file '{}': {}", input_file, e));
118                return 1;
119            }
120        }
121    } else {
122        None
123    };
124
125    // Prepare output destination
126    let mut output_writer: Box<dyn Write> = if let Some(override_writer) = output_override {
127        override_writer
128    } else if let Some(ref output_file) = cmd.output {
129        // Files don't get colors
130        match File::create(output_file) {
131            Ok(file) => Box::new(file),
132            Err(e) => {
133                print_error(&format!(
134                    "Error creating output file '{}': {}",
135                    output_file, e
136                ));
137                return 1;
138            }
139        }
140    } else if let Some(ref append_file) = cmd.append {
141        // Files don't get colors
142        match File::options().append(true).create(true).open(append_file) {
143            Ok(file) => Box::new(file),
144            Err(e) => {
145                print_error(&format!(
146                    "Error opening append file '{}': {}",
147                    append_file, e
148                ));
149                return 1;
150            }
151        }
152    } else {
153        // Terminal output
154        Box::new(ColoredWriter::new(io::stdout()))
155    };
156
157    let builtins = get_builtins();
158    if let Some(builtin) = builtins
159        .into_iter()
160        .find(|b| b.names().contains(&cmd.args[0].as_str()))
161    {
162        builtin.run(cmd, shell_state, &mut *output_writer)
163    } else {
164        1
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_is_builtin() {
174        assert!(is_builtin("cd"));
175        assert!(is_builtin("pwd"));
176        assert!(is_builtin("env"));
177        assert!(is_builtin("exit"));
178        assert!(is_builtin("help"));
179        assert!(is_builtin("alias"));
180        assert!(is_builtin("unalias"));
181        assert!(is_builtin("test"));
182        assert!(is_builtin("["));
183        assert!(is_builtin("."));
184        assert!(!is_builtin("ls"));
185        assert!(!is_builtin("grep"));
186        assert!(!is_builtin("echo"));
187    }
188
189    #[test]
190    fn test_execute_builtin_unknown() {
191        let cmd = ShellCommand {
192            args: vec!["unknown".to_string()],
193            input: None,
194            output: None,
195            append: None,
196        };
197        let mut shell_state = crate::state::ShellState::new();
198        let exit_code = execute_builtin(&cmd, &mut shell_state, None);
199        assert_eq!(exit_code, 1);
200    }
201
202    #[test]
203    fn test_get_builtin_commands() {
204        let commands = get_builtin_commands();
205        assert!(commands.contains(&"cd".to_string()));
206        assert!(commands.contains(&"pwd".to_string()));
207        assert!(commands.contains(&"env".to_string()));
208        assert!(commands.contains(&"exit".to_string()));
209        assert!(commands.contains(&"help".to_string()));
210        assert!(commands.contains(&"source".to_string()));
211        assert!(commands.contains(&"export".to_string()));
212        assert!(commands.contains(&"unset".to_string()));
213        assert!(commands.contains(&"pushd".to_string()));
214        assert!(commands.contains(&"popd".to_string()));
215        assert!(commands.contains(&"dirs".to_string()));
216        assert!(commands.contains(&"alias".to_string()));
217        assert!(commands.contains(&"unalias".to_string()));
218        assert!(commands.contains(&"test".to_string()));
219        assert!(commands.contains(&"[".to_string()));
220        assert!(commands.contains(&".".to_string()));
221        assert!(commands.contains(&"set_colors".to_string()));
222        assert!(commands.contains(&"set_color_scheme".to_string()));
223        assert!(commands.contains(&"set_condensed".to_string()));
224        assert_eq!(commands.len(), 21);
225    }
226}