Skip to main content

brush_core/
builtins.rs

1//! Facilities for implementing and managing builtins
2
3use clap::builder::styling;
4pub use futures::future::BoxFuture;
5use std::io::Write;
6
7use crate::{BuiltinError, CommandArg, commands, error, extensions, results};
8
9/// Type of a function implementing a built-in command.
10///
11/// # Arguments
12///
13/// * The context in which the command is being executed.
14/// * The arguments to the command.
15#[allow(type_alias_bounds)]
16pub type CommandExecuteFunc<SE: extensions::ShellExtensions> =
17    fn(
18        commands::ExecutionContext<'_, SE>,
19        Vec<commands::CommandArg>,
20    ) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>>;
21
22/// Type of a function to retrieve help content for a built-in command.
23///
24/// # Arguments
25///
26/// * `name` - The name of the command.
27/// * `content_type` - The type of content to retrieve.
28/// * `options` - Additional options for content retrieval.
29pub type CommandContentFunc =
30    fn(&str, ContentType, &ContentOptions) -> Result<String, error::Error>;
31
32/// Trait implemented by built-in shell commands.
33pub trait Command: clap::Parser {
34    /// The error type returned by the command.
35    type Error: BuiltinError + 'static;
36
37    /// Instantiates the built-in command with the given arguments.
38    ///
39    /// # Arguments
40    ///
41    /// * `args` - The arguments to the command.
42    fn new<I>(args: I) -> Result<Self, clap::Error>
43    where
44        I: IntoIterator<Item = String>,
45    {
46        if !Self::takes_plus_options() {
47            Self::try_parse_from(args)
48        } else {
49            // N.B. clap doesn't support named options like '+x'. To work around this, we
50            // establish a pattern of renaming them.
51            let mut updated_args = vec![];
52            for arg in args {
53                if let Some(plus_options) = arg.strip_prefix("+") {
54                    for c in plus_options.chars() {
55                        updated_args.push(format!("--+{c}"));
56                    }
57                } else {
58                    updated_args.push(arg);
59                }
60            }
61
62            Self::try_parse_from(updated_args)
63        }
64    }
65
66    /// Returns whether or not the command takes options with a leading '+' or '-' character.
67    fn takes_plus_options() -> bool {
68        false
69    }
70
71    /// Executes the built-in command in the provided context.
72    ///
73    /// # Arguments
74    ///
75    /// * `context` - The context in which the command is being executed.
76    // NOTE: we use desugared async here because we need a Send marker
77    fn execute<SE: extensions::ShellExtensions>(
78        &self,
79        context: commands::ExecutionContext<'_, SE>,
80    ) -> impl std::future::Future<Output = Result<results::ExecutionResult, Self::Error>>
81    + std::marker::Send;
82
83    /// Returns the textual help content associated with the command.
84    ///
85    /// # Arguments
86    ///
87    /// * `name` - The name of the command.
88    /// * `content_type` - The type of content to retrieve.
89    /// * `options` - Additional options for content retrieval.
90    fn get_content(
91        name: &str,
92        content_type: ContentType,
93        options: &ContentOptions,
94    ) -> Result<String, error::Error> {
95        let mut clap_command = Self::command()
96            .styles(brush_help_styles())
97            .next_line_help(false);
98        clap_command.set_bin_name(name);
99
100        let s = match content_type {
101            ContentType::DetailedHelp => {
102                let rendered = clap_command.render_help();
103                if options.colorized {
104                    rendered.ansi().to_string()
105                } else {
106                    rendered.to_string()
107                }
108            }
109            ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
110            ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
111            ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
112        };
113
114        Ok(s)
115    }
116}
117
118/// Trait implemented by built-in shell commands that take specially handled declarations
119/// as arguments.
120pub trait DeclarationCommand: Command {
121    /// Stores the declarations within the command instance.
122    ///
123    /// # Arguments
124    ///
125    /// * `declarations` - The declarations to store.
126    fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
127}
128
129/// Type of help content, typically associated with a built-in command.
130pub enum ContentType {
131    /// Detailed help content for the command.
132    DetailedHelp,
133    /// Short usage information for the command.
134    ShortUsage,
135    /// Short description for the command.
136    ShortDescription,
137    /// man-style help page.
138    ManPage,
139}
140
141/// Options for retrieving built-in command content.
142#[derive(Default)]
143pub struct ContentOptions {
144    /// Whether or not the content should be colorized.
145    pub colorized: bool,
146}
147
148/// Encapsulates a registration for a built-in command.
149#[derive(Clone)]
150pub struct Registration<SE: extensions::ShellExtensions> {
151    /// Function to execute the builtin.
152    pub execute_func: CommandExecuteFunc<SE>,
153
154    /// Function to retrieve the builtin's content/help text.
155    pub content_func: CommandContentFunc,
156
157    /// Has this registration been disabled?
158    pub disabled: bool,
159
160    /// Is the builtin classified as "special" by specification?
161    pub special_builtin: bool,
162
163    /// Is this builtin one that takes specially handled declarations?
164    pub declaration_builtin: bool,
165}
166
167impl<SE: extensions::ShellExtensions> Registration<SE> {
168    /// Updates the given registration to mark it for a special builtin.
169    #[must_use]
170    pub const fn special(self) -> Self {
171        Self {
172            special_builtin: true,
173            ..self
174        }
175    }
176}
177
178fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
179    error::unimp("man page rendering is not yet implemented")
180}
181
182fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
183    let about = command
184        .get_about()
185        .map_or_else(String::new, |s| s.to_string());
186
187    std::format!("{name} - {about}\n")
188}
189
190fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
191    let mut usage = String::new();
192
193    let mut needs_space = false;
194
195    let mut optional_short_opts = vec![];
196    let mut required_short_opts = vec![];
197    for opt in command.get_opts() {
198        if opt.is_hide_set() {
199            continue;
200        }
201
202        if let Some(c) = opt.get_short() {
203            if !opt.is_required_set() {
204                optional_short_opts.push(c);
205            } else {
206                required_short_opts.push(c);
207            }
208        }
209    }
210
211    if !optional_short_opts.is_empty() {
212        if needs_space {
213            usage.push(' ');
214        }
215
216        usage.push('[');
217        usage.push('-');
218        for c in optional_short_opts {
219            usage.push(c);
220        }
221
222        usage.push(']');
223        needs_space = true;
224    }
225
226    if !required_short_opts.is_empty() {
227        if needs_space {
228            usage.push(' ');
229        }
230
231        usage.push('-');
232        for c in required_short_opts {
233            usage.push(c);
234        }
235
236        needs_space = true;
237    }
238
239    for pos in command.get_positionals() {
240        if pos.is_hide_set() {
241            continue;
242        }
243
244        if !pos.is_required_set() {
245            if needs_space {
246                usage.push(' ');
247            }
248
249            usage.push('[');
250            needs_space = false;
251        }
252
253        if let Some(names) = pos.get_value_names() {
254            for name in names {
255                if needs_space {
256                    usage.push(' ');
257                }
258
259                usage.push_str(name);
260                needs_space = true;
261            }
262        }
263
264        if !pos.is_required_set() {
265            usage.push(']');
266            needs_space = true;
267        }
268    }
269
270    std::format!("{name}: {name} {usage}\n")
271}
272
273fn brush_help_styles() -> clap::builder::Styles {
274    styling::Styles::styled()
275        .header(
276            styling::AnsiColor::Yellow.on_default()
277                | styling::Effects::BOLD
278                | styling::Effects::UNDERLINE,
279        )
280        .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
281        .literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
282        .placeholder(styling::AnsiColor::Cyan.on_default())
283}
284
285/// This function and the [`try_parse_known`] exists to deal with
286/// the Clap's limitation of treating `--` like a regular value
287/// `https://github.com/clap-rs/clap/issues/5055`
288///
289/// # Arguments
290///
291/// * `args` - An Iterator from [`std::env::args`]
292///
293/// # Returns
294///
295/// * a parsed struct T from [`clap::Parser::parse_from`]
296/// * the remain iterator `args` with `--` and the rest arguments if they present otherwise None
297///
298/// # Examples
299/// ```
300///    use clap::{builder::styling, Parser};
301///    #[derive(Parser)]
302///    struct CommandLineArgs {
303///       #[clap(allow_hyphen_values = true, num_args=1..)]
304///       script_args: Vec<String>,
305///    }
306///
307///    let (mut parsed_args, raw_args) =
308///        brush_core::builtins::parse_known::<CommandLineArgs, _>(std::env::args());
309///    if raw_args.is_some() {
310///        parsed_args.script_args = raw_args.unwrap().collect();
311///    }
312/// ```
313pub fn parse_known<T: clap::Parser, S>(
314    args: impl IntoIterator<Item = S>,
315) -> (T, Option<impl Iterator<Item = S>>)
316where
317    S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
318{
319    let mut args = args.into_iter();
320    // the best way to save `--` is to get it out with a side effect while `clap` iterates over the
321    // args this way we can be 100% sure that we have '--' and the remaining args
322    // and we will iterate only once
323    let mut hyphen = None;
324    let args_before_hyphen = args.by_ref().take_while(|a| {
325        let is_hyphen = *a == "--";
326        if is_hyphen {
327            hyphen = Some(a.clone());
328        }
329        !is_hyphen
330    });
331    let parsed_args = T::parse_from(args_before_hyphen);
332    let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
333    (parsed_args, raw_args)
334}
335
336/// Similar to [`parse_known`] but with [`clap::Parser::try_parse_from`]
337/// This function is used to parse arguments in builtins such as
338/// `crate::echo::EchoCommand`
339pub fn try_parse_known<T: clap::Parser>(
340    args: impl IntoIterator<Item = String>,
341) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
342    let mut args = args.into_iter();
343    let mut hyphen = None;
344    let args_before_hyphen = args.by_ref().take_while(|a| {
345        let is_hyphen = a == "--";
346        if is_hyphen {
347            hyphen = Some(a.clone());
348        }
349        !is_hyphen
350    });
351    let parsed_args = T::try_parse_from(args_before_hyphen)?;
352
353    let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
354    Ok((parsed_args, raw_args))
355}
356
357/// A simple command that can be registered as a built-in.
358pub trait SimpleCommand {
359    /// Returns the content of the built-in command.
360    fn get_content(
361        name: &str,
362        content_type: ContentType,
363        options: &ContentOptions,
364    ) -> Result<String, error::Error>;
365
366    /// Executes the built-in command.
367    fn execute<SE: extensions::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
368        context: commands::ExecutionContext<'_, SE>,
369        args: I,
370    ) -> Result<results::ExecutionResult, error::Error>;
371}
372
373/// Returns a built-in command registration, given an implementation of the
374/// `SimpleCommand` trait.
375pub fn simple_builtin<B: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>()
376-> Registration<SE> {
377    Registration {
378        execute_func: exec_simple_builtin::<B, SE>,
379        content_func: B::get_content,
380        disabled: false,
381        special_builtin: false,
382        declaration_builtin: false,
383    }
384}
385
386/// Returns a built-in command registration, given an implementation of the
387/// `Command` trait.
388pub fn builtin<B: Command + Send + Sync, SE: extensions::ShellExtensions>() -> Registration<SE> {
389    Registration {
390        execute_func: exec_builtin::<B, SE>,
391        content_func: get_builtin_content::<B>,
392        disabled: false,
393        special_builtin: false,
394        declaration_builtin: false,
395    }
396}
397
398/// Returns a built-in command registration, given an implementation of the
399/// `DeclarationCommand` trait. Used for select commands that can take parsed
400/// declarations as arguments.
401pub fn decl_builtin<B: DeclarationCommand + Send + Sync, SE: extensions::ShellExtensions>()
402-> Registration<SE> {
403    Registration {
404        execute_func: exec_declaration_builtin::<B, SE>,
405        content_func: get_builtin_content::<B>,
406        disabled: false,
407        special_builtin: false,
408        declaration_builtin: true,
409    }
410}
411
412#[allow(clippy::too_long_first_doc_paragraph)]
413/// Returns a built-in command registration, given an implementation of the
414/// `DeclarationCommand` trait that can be default-constructed. The command
415/// implementation is expected to implement clap's `Parser` trait solely
416/// for help/usage information. Arguments are passed directly to the command
417/// via `set_declarations`. This is primarily only expected to be used with
418/// select builtin commands that wrap other builtins (e.g., "builtin").
419pub fn raw_arg_builtin<
420    B: DeclarationCommand + Default + Send + Sync,
421    SE: extensions::ShellExtensions,
422>() -> Registration<SE> {
423    Registration {
424        execute_func: exec_raw_arg_builtin::<B, SE>,
425        content_func: get_builtin_content::<B>,
426        disabled: false,
427        special_builtin: false,
428        declaration_builtin: true,
429    }
430}
431
432fn get_builtin_content<T: Command + Send + Sync>(
433    name: &str,
434    content_type: ContentType,
435    options: &ContentOptions,
436) -> Result<String, error::Error> {
437    T::get_content(name, content_type, options)
438}
439
440fn exec_simple_builtin<T: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>(
441    context: commands::ExecutionContext<'_, SE>,
442    args: Vec<CommandArg>,
443) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
444    Box::pin(async move { exec_simple_builtin_impl::<T, SE>(context, args).await })
445}
446
447#[expect(clippy::unused_async)]
448async fn exec_simple_builtin_impl<
449    T: SimpleCommand + Send + Sync,
450    SE: extensions::ShellExtensions,
451>(
452    context: commands::ExecutionContext<'_, SE>,
453    args: Vec<CommandArg>,
454) -> Result<results::ExecutionResult, error::Error> {
455    let plain_args = args.into_iter().map(|arg| match arg {
456        CommandArg::String(s) => s,
457        CommandArg::Assignment(a) => a.to_string(),
458    });
459
460    T::execute(context, plain_args)
461}
462
463fn exec_builtin<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
464    context: commands::ExecutionContext<'_, SE>,
465    args: Vec<CommandArg>,
466) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
467    Box::pin(async move { exec_builtin_impl::<T, SE>(context, args).await })
468}
469
470async fn exec_builtin_impl<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
471    context: commands::ExecutionContext<'_, SE>,
472    args: Vec<CommandArg>,
473) -> Result<results::ExecutionResult, error::Error> {
474    let plain_args = args.into_iter().map(|arg| match arg {
475        CommandArg::String(s) => s,
476        CommandArg::Assignment(a) => a.to_string(),
477    });
478
479    let result = T::new(plain_args);
480    let command = match result {
481        Ok(command) => command,
482        Err(e) => {
483            let _ = writeln!(context.stderr(), "{e}");
484            return Ok(results::ExecutionExitCode::InvalidUsage.into());
485        }
486    };
487
488    call_builtin(command, context).await
489}
490
491fn exec_declaration_builtin<
492    T: DeclarationCommand + Send + Sync,
493    SE: extensions::ShellExtensions,
494>(
495    context: commands::ExecutionContext<'_, SE>,
496    args: Vec<CommandArg>,
497) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
498    Box::pin(async move { exec_declaration_builtin_impl::<T, SE>(context, args).await })
499}
500
501async fn exec_declaration_builtin_impl<
502    T: DeclarationCommand + Send + Sync,
503    SE: extensions::ShellExtensions,
504>(
505    context: commands::ExecutionContext<'_, SE>,
506    args: Vec<CommandArg>,
507) -> Result<results::ExecutionResult, error::Error> {
508    let mut options = vec![];
509    let mut declarations = vec![];
510
511    for (i, arg) in args.into_iter().enumerate() {
512        match arg {
513            CommandArg::String(s)
514                if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
515            {
516                options.push(s);
517            }
518            _ => declarations.push(arg),
519        }
520    }
521
522    let result = T::new(options);
523    let mut command = match result {
524        Ok(command) => command,
525        Err(e) => {
526            let _ = writeln!(context.stderr(), "{e}");
527            return Ok(results::ExecutionExitCode::InvalidUsage.into());
528        }
529    };
530
531    command.set_declarations(declarations);
532
533    call_builtin(command, context).await
534}
535
536fn exec_raw_arg_builtin<
537    T: DeclarationCommand + Default + Send + Sync,
538    SE: extensions::ShellExtensions,
539>(
540    context: commands::ExecutionContext<'_, SE>,
541    args: Vec<CommandArg>,
542) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
543    Box::pin(async move { exec_raw_arg_builtin_impl::<T, SE>(context, args).await })
544}
545
546async fn exec_raw_arg_builtin_impl<
547    T: DeclarationCommand + Default + Send + Sync,
548    SE: extensions::ShellExtensions,
549>(
550    context: commands::ExecutionContext<'_, SE>,
551    args: Vec<CommandArg>,
552) -> Result<results::ExecutionResult, error::Error> {
553    let mut command = T::default();
554    command.set_declarations(args);
555
556    call_builtin(command, context).await
557}
558
559async fn call_builtin(
560    command: impl Command,
561    context: commands::ExecutionContext<'_, impl extensions::ShellExtensions>,
562) -> Result<results::ExecutionResult, error::Error> {
563    let builtin_name = context.command_name.clone();
564    let result = command
565        .execute(context)
566        .await
567        .map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
568
569    Ok(result)
570}