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