brush_core/
builtins.rs

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