Skip to main content

vecli/
commands.rs

1//! Command definitions and the [`CommandContext`] type.
2//!
3//! [`Command`] represents a single subcommand registered on an [`crate::App`].
4//! [`CommandContext`] is the parsed invocation context delivered to every handler.
5
6use crate::flags::Flag;
7
8/// A single registered subcommand.
9///
10/// Build with [`Command::new`] and configure via the builder methods before
11/// passing to [`App::add_command`].
12#[derive(Default)]
13pub struct Command {
14    pub(crate) name: String,
15    pub(crate) description: String,
16    pub(crate) known_flags: Vec<Flag>,
17    pub(crate) usage: Option<String>,
18    pub(crate) handler: Option<fn(&CommandContext)>,
19    pub(crate) strict_flags: bool,
20    pub(crate) subcommands: Vec<Command>,
21    pub(crate) print_help_if_no_args: bool,
22}
23
24impl Command {
25    // Constructors
26    /// Creates a new command with the given name and handler function.
27    ///
28    /// The `handler` receives a [`CommandContext`] containing the resolved flags
29    /// and positional arguments for this invocation.
30    pub fn new(name: impl Into<String>, handler: fn(&CommandContext)) -> Self {
31        Self {
32            name: name.into(),
33            handler: Some(handler),
34            ..Default::default()
35        }
36    }
37
38    pub fn parent(name: impl Into<String>) -> Self {
39        Self {
40            name: name.into(),
41            ..Default::default()
42        }
43    }
44
45    /// Sets the short description shown in the app-level help listing.
46    pub fn description(mut self, description: impl Into<String>) -> Self {
47        self.description = description.into();
48        self
49    }
50
51    /// Sets the usage string shown when the user runs `<cmd> --help`.
52    ///
53    /// Displayed as: `<prog> <cmd> <usage>`. For example, passing `"<file> [--output <path>]"`
54    /// produces `mytool convert <file> [--output <path>]`.
55    ///
56    /// If omitted and the command has registered flags, a fallback of `[options]` is shown.
57    pub fn usage(mut self, usage: impl Into<String>) -> Self {
58        self.usage = Some(usage.into());
59        self
60    }
61
62    /// Controls whether unknown flags cause a hard error or just a warning.
63    ///
64    /// When `true`, passing an unrecognized flag prints an error and exits without
65    /// calling the handler. When `false` (the default), a warning is printed and
66    /// execution continues. Global flags are always considered known and never
67    /// trigger this check.
68    pub fn strict_flags(mut self, strict: bool) -> Self {
69        self.strict_flags = strict;
70        self
71    }
72
73    /// Registers a flag definition for this command.
74    ///
75    /// Registered flags participate in alias resolution and appear in help text.
76    /// Can be called multiple times to register multiple flags.
77    pub fn flag(mut self, flag: Flag) -> Self {
78        self.known_flags.push(flag);
79        self
80    }
81
82    pub fn subcommand(mut self, subcommand: Command) -> Self {
83        self.subcommands.push(subcommand);
84        self
85    }
86
87    pub fn print_help_if_no_args(mut self, print: bool) -> Self {
88        self.print_help_if_no_args = print;
89        self
90    }
91
92    /// Prints help text for this command to stdout.
93    ///
94    /// Output includes the usage line, description, and a formatted flag listing.
95    /// If no usage string was set but flags are registered, a `[options]` fallback
96    /// is used for the usage line.
97    pub(crate) fn print_help(&self, prog: &str) {
98        if let Some(usage) = &self.usage {
99            println!("USAGE: {} {} {}", prog, self.name, usage);
100        } else if !self.known_flags.is_empty() {
101            // fallback that still makes sense
102            println!("USAGE: {} {} [options]", prog, self.name);
103        }
104        println!("    {}", self.description);
105        println!();
106        if !self.known_flags.is_empty() {
107            println!("OPTIONS:");
108
109            let longest = self
110                .known_flags
111                .iter()
112                .map(|f| {
113                    let alias_part = f.alias.as_ref().map_or(0, |a| a.len() + 4);
114                    f.name.len() + alias_part
115                })
116                .max()
117                .unwrap_or(0);
118
119            for flag in &self.known_flags {
120                let left = if let Some(alias) = &flag.alias {
121                    format!("--{}, -{}", flag.name, alias)
122                } else {
123                    format!("--{}", flag.name)
124                };
125                let description = flag.description.as_deref().unwrap_or("");
126                println!("    {:<width$} {}", left, description, width = longest + 10);
127            }
128        }
129        if !self.subcommands.is_empty() {
130            println!();
131            println!("SUBCOMMANDS:");
132
133            let longest = self
134                .subcommands
135                .iter()
136                .map(|s| s.name.len())
137                .max()
138                .unwrap_or(0)
139                + 10;
140
141            for subcommand in &self.subcommands {
142                println!(
143                    "    {:<width$} {}",
144                    subcommand.name,
145                    subcommand.description,
146                    width = longest
147                );
148            }
149        }
150    }
151}
152
153/// Holds the parsed context for a command invocation.
154///
155/// Passed by reference to every command handler. Contains the resolved subcommand
156/// name, any positional arguments (non-flag tokens), and the full set of flags
157/// after alias resolution. Global flags registered on the app are merged in
158/// alongside command-specific flags.
159pub struct CommandContext {
160    /// The subcommand name as typed by the user.
161    pub subcommand: String,
162    /// Positional arguments, in order, with flags filtered out.
163    pub positionals: Vec<String>,
164    /// Resolved flags, keyed by canonical name. Boolean flags have the value `"true"`.
165    /// Includes both command-specific flags and any global flags that were passed.
166    pub flags: std::collections::HashMap<String, String>,
167}