Skip to main content

shrub_core/
builtins.rs

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