brush_core/
builtins.rs

1//! Infrastructure for shell built-in commands.
2
3use clap::builder::styling;
4use clap::Parser;
5use futures::future::BoxFuture;
6
7use crate::commands;
8use crate::error;
9use crate::ExecutionResult;
10
11mod alias;
12mod bg;
13mod bind;
14mod break_;
15mod brushinfo;
16mod builtin_;
17mod cd;
18mod colon;
19mod command;
20mod complete;
21mod continue_;
22mod declare;
23mod dirs;
24mod dot;
25mod echo;
26mod enable;
27mod eval;
28#[cfg(unix)]
29mod exec;
30mod exit;
31mod export;
32mod factory;
33mod false_;
34mod fg;
35mod getopts;
36mod hash;
37mod help;
38mod jobs;
39#[cfg(unix)]
40mod kill;
41mod let_;
42mod mapfile;
43mod popd;
44mod printf;
45mod pushd;
46mod pwd;
47mod read;
48mod return_;
49mod set;
50mod shift;
51mod shopt;
52#[cfg(unix)]
53mod suspend;
54mod test;
55mod times;
56mod trap;
57mod true_;
58mod type_;
59#[cfg(unix)]
60mod umask;
61mod unalias;
62mod unimp;
63mod unset;
64mod wait;
65
66pub(crate) use factory::get_default_builtins;
67pub use factory::{builtin, simple_builtin, SimpleCommand};
68
69/// Macro to define a struct that represents a shell built-in flag argument that can be
70/// enabled or disabled by specifying an option with a leading '+' or '-' character.
71///
72/// # Arguments
73///
74/// - `$struct_name` - The identifier to be used for the struct to define.
75/// - `$flag_char` - The character to use as the flag.
76/// - `$desc` - The string description of the flag.
77#[macro_export]
78macro_rules! minus_or_plus_flag_arg {
79    ($struct_name:ident, $flag_char:literal, $desc:literal) => {
80        #[derive(clap::Parser)]
81        pub(crate) struct $struct_name {
82            #[arg(short = $flag_char, name = concat!(stringify!($struct_name), "_enable"), action = clap::ArgAction::SetTrue, help = $desc)]
83            _enable: bool,
84            #[arg(long = concat!("+", $flag_char), name = concat!(stringify!($struct_name), "_disable"), action = clap::ArgAction::SetTrue, hide = true)]
85            _disable: bool,
86        }
87
88        impl From<$struct_name> for Option<bool> {
89            fn from(value: $struct_name) -> Self {
90                value.to_bool()
91            }
92        }
93
94        impl $struct_name {
95            #[allow(dead_code)]
96            pub fn is_some(&self) -> bool {
97                self._enable || self._disable
98            }
99
100            pub fn to_bool(&self) -> Option<bool> {
101                match (self._enable, self._disable) {
102                    (true, false) => Some(true),
103                    (false, true) => Some(false),
104                    _ => None,
105                }
106            }
107        }
108    };
109}
110
111pub(crate) use minus_or_plus_flag_arg;
112
113/// Result of executing a built-in command.
114#[allow(clippy::module_name_repetitions)]
115pub struct BuiltinResult {
116    /// The exit code from the command.
117    pub exit_code: ExitCode,
118}
119
120/// Exit codes for built-in commands.
121pub enum ExitCode {
122    /// The command was successful.
123    Success,
124    /// The inputs to the command were invalid.
125    InvalidUsage,
126    /// The command is not implemented.
127    Unimplemented,
128    /// The command returned a specific custom numerical exit code.
129    Custom(u8),
130    /// The command is requesting to exit the shell, yielding the given exit code.
131    ExitShell(u8),
132    /// The command is requesting to return from a function or script, yielding the given exit
133    /// code.
134    ReturnFromFunctionOrScript(u8),
135    /// The command is requesting to continue a loop, identified by the given nesting count.
136    ContinueLoop(u8),
137    /// The command is requesting to break a loop, identified by the given nesting count.
138    BreakLoop(u8),
139}
140
141impl From<ExecutionResult> for ExitCode {
142    fn from(result: ExecutionResult) -> Self {
143        if let Some(count) = result.continue_loop {
144            ExitCode::ContinueLoop(count)
145        } else if let Some(count) = result.break_loop {
146            ExitCode::BreakLoop(count)
147        } else if result.return_from_function_or_script {
148            ExitCode::ReturnFromFunctionOrScript(result.exit_code)
149        } else if result.exit_shell {
150            ExitCode::ExitShell(result.exit_code)
151        } else if result.exit_code == 0 {
152            ExitCode::Success
153        } else {
154            ExitCode::Custom(result.exit_code)
155        }
156    }
157}
158
159/// Type of a function implementing a built-in command.
160///
161/// # Arguments
162///
163/// * The context in which the command is being executed.
164/// * The arguments to the command.
165pub type CommandExecuteFunc = fn(
166    commands::ExecutionContext<'_>,
167    Vec<commands::CommandArg>,
168) -> BoxFuture<'_, Result<BuiltinResult, error::Error>>;
169
170/// Type of a function to retrieve help content for a built-in command.
171///
172/// # Arguments
173///
174/// * `name` - The name of the command.
175/// * `content_type` - The type of content to retrieve.
176pub type CommandContentFunc = fn(&str, ContentType) -> Result<String, error::Error>;
177
178/// Trait implemented by built-in shell commands.
179pub trait Command: Parser {
180    /// Instantiates the built-in command with the given arguments.
181    ///
182    /// # Arguments
183    ///
184    /// * `args` - The arguments to the command.
185    fn new<I>(args: I) -> Result<Self, clap::Error>
186    where
187        I: IntoIterator<Item = String>,
188    {
189        if !Self::takes_plus_options() {
190            Self::try_parse_from(args)
191        } else {
192            // N.B. clap doesn't support named options like '+x'. To work around this, we
193            // establish a pattern of renaming them.
194            let mut updated_args = vec![];
195            for arg in args {
196                if let Some(plus_options) = arg.strip_prefix("+") {
197                    for c in plus_options.chars() {
198                        updated_args.push(format!("--+{c}"));
199                    }
200                } else {
201                    updated_args.push(arg);
202                }
203            }
204
205            Self::try_parse_from(updated_args)
206        }
207    }
208
209    /// Returns whether or not the command takes options with a leading '+' or '-' character.
210    fn takes_plus_options() -> bool {
211        false
212    }
213
214    /// Executes the built-in command in the provided context.
215    ///
216    /// # Arguments
217    ///
218    /// * `context` - The context in which the command is being executed.
219    // NOTE: we use desugared async here because we need a Send marker
220    fn execute(
221        &self,
222        context: commands::ExecutionContext<'_>,
223    ) -> impl std::future::Future<Output = Result<ExitCode, error::Error>> + std::marker::Send;
224
225    /// Returns the textual help content associated with the command.
226    ///
227    /// # Arguments
228    ///
229    /// * `name` - The name of the command.
230    /// * `content_type` - The type of content to retrieve.
231    fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error> {
232        let mut clap_command = Self::command().styles(brush_help_styles());
233        clap_command.set_bin_name(name);
234
235        let s = match content_type {
236            ContentType::DetailedHelp => clap_command.render_long_help().ansi().to_string(),
237            ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
238            ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
239            ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
240        };
241
242        Ok(s)
243    }
244}
245
246/// Trait implemented by built-in shell commands that take specially handled declarations
247/// as arguments.
248pub trait DeclarationCommand: Command {
249    /// Stores the declarations within the command instance.
250    ///
251    /// # Arguments
252    ///
253    /// * `declarations` - The declarations to store.
254    fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
255}
256
257/// Type of help content, typically associated with a built-in command.
258pub enum ContentType {
259    /// Detailed help content for the command.
260    DetailedHelp,
261    /// Short usage information for the command.
262    ShortUsage,
263    /// Short description for the command.
264    ShortDescription,
265    /// man-style help page.
266    ManPage,
267}
268
269/// Encapsulates a registration for a built-in command.
270#[derive(Clone)]
271pub struct Registration {
272    /// Function to execute the builtin.
273    pub execute_func: CommandExecuteFunc,
274
275    /// Function to retrieve the builtin's content/help text.
276    pub content_func: CommandContentFunc,
277
278    /// Has this registration been disabled?
279    pub disabled: bool,
280
281    /// Is the builtin classified as "special" by specification?
282    pub special_builtin: bool,
283
284    /// Is this builtin one that takes specially handled declarations?
285    pub declaration_builtin: bool,
286}
287
288impl Registration {
289    /// Updates the given registration to mark it for a special builtin.
290    #[must_use]
291    pub fn special(self) -> Self {
292        Self {
293            special_builtin: true,
294            ..self
295        }
296    }
297}
298
299fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
300    error::unimp("man page rendering is not yet implemented")
301}
302
303fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
304    let about = command
305        .get_about()
306        .map_or_else(String::new, |s| s.to_string());
307
308    std::format!("{name} - {about}\n")
309}
310
311fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
312    let mut usage = String::new();
313
314    let mut needs_space = false;
315
316    let mut optional_short_opts = vec![];
317    let mut required_short_opts = vec![];
318    for opt in command.get_opts() {
319        if opt.is_hide_set() {
320            continue;
321        }
322
323        if let Some(c) = opt.get_short() {
324            if !opt.is_required_set() {
325                optional_short_opts.push(c);
326            } else {
327                required_short_opts.push(c);
328            }
329        }
330    }
331
332    if !optional_short_opts.is_empty() {
333        if needs_space {
334            usage.push(' ');
335        }
336
337        usage.push('[');
338        usage.push('-');
339        for c in optional_short_opts {
340            usage.push(c);
341        }
342
343        usage.push(']');
344        needs_space = true;
345    }
346
347    if !required_short_opts.is_empty() {
348        if needs_space {
349            usage.push(' ');
350        }
351
352        usage.push('-');
353        for c in required_short_opts {
354            usage.push(c);
355        }
356
357        needs_space = true;
358    }
359
360    for pos in command.get_positionals() {
361        if pos.is_hide_set() {
362            continue;
363        }
364
365        if !pos.is_required_set() {
366            if needs_space {
367                usage.push(' ');
368            }
369
370            usage.push('[');
371            needs_space = false;
372        }
373
374        if let Some(names) = pos.get_value_names() {
375            for name in names {
376                if needs_space {
377                    usage.push(' ');
378                }
379
380                usage.push_str(name);
381                needs_space = true;
382            }
383        }
384
385        if !pos.is_required_set() {
386            usage.push(']');
387            needs_space = true;
388        }
389    }
390
391    std::format!("{name}: {name} {usage}\n")
392}
393
394fn brush_help_styles() -> clap::builder::Styles {
395    styling::Styles::styled()
396        .header(
397            styling::AnsiColor::Yellow.on_default()
398                | styling::Effects::BOLD
399                | styling::Effects::UNDERLINE,
400        )
401        .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
402        .literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
403        .placeholder(styling::AnsiColor::Cyan.on_default())
404}
405
406/// This function and the [`try_parse_known`] exists to deal with
407/// the Clap's limitation of treating `--` like a regular value
408/// `https://github.com/clap-rs/clap/issues/5055`
409///
410/// # Arguments
411///
412/// * `args` - An Iterator from [`std::env::args`]
413///
414/// # Returns
415///
416/// * a parsed struct T from [`clap::Parser::parse_from`]
417/// * the remain iterator `args` with `--` and the rest arguments if they present othervise None
418///
419/// # Examples
420/// ```
421///    use clap::{builder::styling, Parser};
422///    #[derive(Parser)]
423///    struct CommandLineArgs {
424///       #[clap(allow_hyphen_values = true, num_args=1..)]
425///       script_args: Vec<String>,
426///    }
427///
428///    let (mut parsed_args, raw_args) =
429///        brush_core::builtins::parse_known::<CommandLineArgs, _>(std::env::args());
430///    if raw_args.is_some() {
431///        parsed_args.script_args = raw_args.unwrap().collect();
432///    }
433/// ```
434pub fn parse_known<T: Parser, S>(
435    args: impl IntoIterator<Item = S>,
436) -> (T, Option<impl Iterator<Item = S>>)
437where
438    S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
439{
440    let mut args = args.into_iter();
441    // the best way to save `--` is to get it out with a side effect while `clap` iterates over the
442    // args this way we can be 100% sure that we have '--' and the remaining args
443    // and we will iterate only once
444    let mut hyphen = None;
445    let args_before_hyphen = args.by_ref().take_while(|a| {
446        let is_hyphen = *a == "--";
447        if is_hyphen {
448            hyphen = Some(a.clone());
449        }
450        !is_hyphen
451    });
452    let parsed_args = T::parse_from(args_before_hyphen);
453    let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
454    (parsed_args, raw_args)
455}
456
457/// Similar to [`parse_known`] but with [`clap::Parser::try_parse_from`]
458/// This function is used to parse arguments in builtins such as
459/// [`crate::builtins::echo::EchoCommand`]
460pub fn try_parse_known<T: Parser>(
461    args: impl IntoIterator<Item = String>,
462) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
463    let mut args = args.into_iter();
464    let mut hyphen = None;
465    let args_before_hyphen = args.by_ref().take_while(|a| {
466        let is_hyphen = a == "--";
467        if is_hyphen {
468            hyphen = Some(a.clone());
469        }
470        !is_hyphen
471    });
472    let parsed_args = T::try_parse_from(args_before_hyphen)?;
473
474    let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
475    Ok((parsed_args, raw_args))
476}