kodegen_bash_shell/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 super::{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/// ```no_run
277/// use clap::Parser;
278/// use kodegen_bash_shell::core::builtins::parse_known;
279///
280/// #[derive(Parser)]
281/// struct CommandLineArgs {
282///    #[clap(allow_hyphen_values = true, num_args=1..)]
283///    script_args: Vec<String>,
284/// }
285///
286/// let (mut parsed_args, raw_args) =
287///     parse_known::<CommandLineArgs, _>(std::env::args());
288/// if let Some(args) = raw_args {
289///     parsed_args.script_args = args.collect();
290/// }
291/// ```
292pub fn parse_known<T: clap::Parser, S>(
293    args: impl IntoIterator<Item = S>,
294) -> (T, Option<impl Iterator<Item = S>>)
295where
296    S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
297{
298    let mut args = args.into_iter();
299    // the best way to save `--` is to get it out with a side effect while `clap` iterates over the
300    // args this way we can be 100% sure that we have '--' and the remaining args
301    // and we will iterate only once
302    let mut hyphen = None;
303    let args_before_hyphen = args.by_ref().take_while(|a| {
304        let is_hyphen = *a == "--";
305        if is_hyphen {
306            hyphen = Some(a.clone());
307        }
308        !is_hyphen
309    });
310    let parsed_args = T::parse_from(args_before_hyphen);
311    let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
312    (parsed_args, raw_args)
313}
314
315/// Similar to [`parse_known`] but with [`clap::Parser::try_parse_from`]
316/// This function is used to parse arguments in builtins such as
317/// `crate::echo::EchoCommand`
318pub fn try_parse_known<T: clap::Parser>(
319    args: impl IntoIterator<Item = String>,
320) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
321    let mut args = args.into_iter();
322    let mut hyphen = None;
323    let args_before_hyphen = args.by_ref().take_while(|a| {
324        let is_hyphen = a == "--";
325        if is_hyphen {
326            hyphen = Some(a.clone());
327        }
328        !is_hyphen
329    });
330    let parsed_args = T::try_parse_from(args_before_hyphen)?;
331
332    let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
333    Ok((parsed_args, raw_args))
334}
335
336/// A simple command that can be registered as a built-in.
337pub trait SimpleCommand {
338    /// Returns the content of the built-in command.
339    fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error>;
340
341    /// Executes the built-in command.
342    fn execute<I: Iterator<Item = S>, S: AsRef<str>>(
343        context: commands::ExecutionContext<'_>,
344        args: I,
345    ) -> Result<results::ExecutionResult, error::Error>;
346}
347
348/// Returns a built-in command registration, given an implementation of the
349/// `SimpleCommand` trait.
350pub fn simple_builtin<B: SimpleCommand + Send + Sync>() -> Registration {
351    Registration {
352        execute_func: exec_simple_builtin::<B>,
353        content_func: B::get_content,
354        disabled: false,
355        special_builtin: false,
356        declaration_builtin: false,
357    }
358}
359
360/// Returns a built-in command registration, given an implementation of the
361/// `Command` trait.
362pub fn builtin<B: Command + Send + Sync>() -> Registration {
363    Registration {
364        execute_func: exec_builtin::<B>,
365        content_func: get_builtin_content::<B>,
366        disabled: false,
367        special_builtin: false,
368        declaration_builtin: false,
369    }
370}
371
372/// Returns a built-in command registration, given an implementation of the
373/// `DeclarationCommand` trait. Used for select commands that can take parsed
374/// declarations as arguments.
375pub fn decl_builtin<B: DeclarationCommand + Send + Sync>() -> Registration {
376    Registration {
377        execute_func: exec_declaration_builtin::<B>,
378        content_func: get_builtin_content::<B>,
379        disabled: false,
380        special_builtin: false,
381        declaration_builtin: true,
382    }
383}
384
385#[allow(clippy::too_long_first_doc_paragraph)]
386/// Returns a built-in command registration, given an implementation of the
387/// `DeclarationCommand` trait that can be default-constructed. The command
388/// implementation is expected to implement clap's `Parser` trait solely
389/// for help/usage information. Arguments are passed directly to the command
390/// via `set_declarations`. This is primarily only expected to be used with
391/// select builtin commands that wrap other builtins (e.g., "builtin").
392pub fn raw_arg_builtin<B: DeclarationCommand + Default + Send + Sync>() -> Registration {
393    Registration {
394        execute_func: exec_raw_arg_builtin::<B>,
395        content_func: get_builtin_content::<B>,
396        disabled: false,
397        special_builtin: false,
398        declaration_builtin: true,
399    }
400}
401
402fn get_builtin_content<T: Command + Send + Sync>(
403    name: &str,
404    content_type: ContentType,
405) -> Result<String, error::Error> {
406    T::get_content(name, content_type)
407}
408
409fn exec_simple_builtin<T: SimpleCommand + Send + Sync>(
410    context: commands::ExecutionContext<'_>,
411    args: Vec<CommandArg>,
412) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
413    Box::pin(async move { exec_simple_builtin_impl::<T>(context, args).await })
414}
415
416#[expect(clippy::unused_async)]
417async fn exec_simple_builtin_impl<T: SimpleCommand + Send + Sync>(
418    context: commands::ExecutionContext<'_>,
419    args: Vec<CommandArg>,
420) -> Result<results::ExecutionResult, error::Error> {
421    let plain_args = args.into_iter().map(|arg| match arg {
422        CommandArg::String(s) => s,
423        CommandArg::Assignment(a) => a.to_string(),
424    });
425
426    T::execute(context, plain_args)
427}
428
429fn exec_builtin<T: Command + Send + Sync>(
430    context: commands::ExecutionContext<'_>,
431    args: Vec<CommandArg>,
432) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
433    Box::pin(async move { exec_builtin_impl::<T>(context, args).await })
434}
435
436async fn exec_builtin_impl<T: Command + Send + Sync>(
437    context: commands::ExecutionContext<'_>,
438    args: Vec<CommandArg>,
439) -> Result<results::ExecutionResult, error::Error> {
440    let plain_args = args.into_iter().map(|arg| match arg {
441        CommandArg::String(s) => s,
442        CommandArg::Assignment(a) => a.to_string(),
443    });
444
445    let result = T::new(plain_args);
446    let command = match result {
447        Ok(command) => command,
448        Err(e) => {
449            writeln!(context.stderr(), "{e}")?;
450            return Ok(results::ExecutionExitCode::InvalidUsage.into());
451        }
452    };
453
454    call_builtin(command, context).await
455}
456
457fn exec_declaration_builtin<T: DeclarationCommand + Send + Sync>(
458    context: commands::ExecutionContext<'_>,
459    args: Vec<CommandArg>,
460) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
461    Box::pin(async move { exec_declaration_builtin_impl::<T>(context, args).await })
462}
463
464async fn exec_declaration_builtin_impl<T: DeclarationCommand + Send + Sync>(
465    context: commands::ExecutionContext<'_>,
466    args: Vec<CommandArg>,
467) -> Result<results::ExecutionResult, error::Error> {
468    let mut options = vec![];
469    let mut declarations = vec![];
470
471    for (i, arg) in args.into_iter().enumerate() {
472        match arg {
473            CommandArg::String(s)
474                if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
475            {
476                options.push(s);
477            }
478            _ => declarations.push(arg),
479        }
480    }
481
482    let result = T::new(options);
483    let mut command = match result {
484        Ok(command) => command,
485        Err(e) => {
486            writeln!(context.stderr(), "{e}")?;
487            return Ok(results::ExecutionExitCode::InvalidUsage.into());
488        }
489    };
490
491    command.set_declarations(declarations);
492
493    call_builtin(command, context).await
494}
495
496fn exec_raw_arg_builtin<T: DeclarationCommand + Default + Send + Sync>(
497    context: commands::ExecutionContext<'_>,
498    args: Vec<CommandArg>,
499) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
500    Box::pin(async move { exec_raw_arg_builtin_impl::<T>(context, args).await })
501}
502
503async fn exec_raw_arg_builtin_impl<T: DeclarationCommand + Default + Send + Sync>(
504    context: commands::ExecutionContext<'_>,
505    args: Vec<CommandArg>,
506) -> Result<results::ExecutionResult, error::Error> {
507    let mut command = T::default();
508    command.set_declarations(args);
509
510    call_builtin(command, context).await
511}
512
513async fn call_builtin(
514    command: impl Command,
515    context: commands::ExecutionContext<'_>,
516) -> Result<results::ExecutionResult, error::Error> {
517    let builtin_name = context.command_name.clone();
518    let result = command
519        .execute(context)
520        .await
521        .map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
522
523    Ok(result)
524}