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