brush_core/
builtins.rs

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