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}