cli_forge/command.rs
1//! The command tree.
2//!
3//! A [`Command`] is one node: a name, optional help text, the [`Arg`]s it
4//! accepts, any nested subcommands, the `hidden` and `requires_auth` flags, and
5//! an optional `run` handler. Commands compose recursively through
6//! [`subcommand`](Command::subcommand), so an arbitrarily deep tree is just
7//! values built with the same builder.
8//!
9//! Commands are registered into an [`App`](crate::App) from anywhere — a command
10//! built in one module behaves identically to one built in `main`.
11
12use std::fmt;
13
14use crate::arg::{Arg, ArgKind};
15use crate::matches::Matches;
16
17/// A handler invoked when its command is the one the user selected.
18type Handler = Box<dyn Fn(&Matches)>;
19
20/// One node in the command tree.
21///
22/// Build with [`Command::new`] and refine with the chaining methods. Attach a
23/// [`run`](Command::run) handler to do the work, [`arg`](Command::arg) to accept
24/// input, and [`subcommand`](Command::subcommand) to nest.
25///
26/// # Examples
27///
28/// ```
29/// use cli_forge::{Arg, Command};
30///
31/// let build = Command::new("build")
32/// .about("compile the project")
33/// .arg(Arg::flag("release").short('r'))
34/// .run(|m| {
35/// let _ = m.flag("release");
36/// });
37/// ```
38pub struct Command {
39 pub(crate) name: String,
40 pub(crate) aliases: Vec<String>,
41 pub(crate) about: Option<String>,
42 pub(crate) args: Vec<Arg>,
43 pub(crate) subcommands: Vec<Command>,
44 pub(crate) hidden: bool,
45 pub(crate) requires_auth: bool,
46 pub(crate) handler: Option<Handler>,
47}
48
49impl Command {
50 /// Create a command with the given invocation name.
51 ///
52 /// # Examples
53 ///
54 /// ```
55 /// use cli_forge::Command;
56 /// let cmd = Command::new("init");
57 /// ```
58 #[must_use]
59 pub fn new(name: impl Into<String>) -> Command {
60 Command {
61 name: name.into(),
62 aliases: Vec::new(),
63 about: None,
64 args: Vec::new(),
65 subcommands: Vec::new(),
66 hidden: false,
67 requires_auth: false,
68 handler: None,
69 }
70 }
71
72 /// Add an alternative name that also invokes this command. Chain it to add
73 /// several. Aliases are shown alongside the name in help.
74 ///
75 /// # Examples
76 ///
77 /// ```
78 /// use cli_forge::Command;
79 /// let cmd = Command::new("remove").alias("rm").alias("del");
80 /// ```
81 #[must_use]
82 pub fn alias(mut self, alias: impl Into<String>) -> Command {
83 self.aliases.push(alias.into());
84 self
85 }
86
87 /// Add several alternative names at once.
88 ///
89 /// # Examples
90 ///
91 /// ```
92 /// use cli_forge::Command;
93 /// let cmd = Command::new("remove").aliases(["rm", "del"]);
94 /// ```
95 #[must_use]
96 pub fn aliases<I, S>(mut self, aliases: I) -> Command
97 where
98 I: IntoIterator<Item = S>,
99 S: Into<String>,
100 {
101 self.aliases.extend(aliases.into_iter().map(Into::into));
102 self
103 }
104
105 /// Set the one-line description shown in help.
106 ///
107 /// # Examples
108 ///
109 /// ```
110 /// use cli_forge::Command;
111 /// let cmd = Command::new("init").about("bootstrap a new project");
112 /// ```
113 #[must_use]
114 pub fn about(mut self, text: impl Into<String>) -> Command {
115 self.about = Some(text.into());
116 self
117 }
118
119 /// Accept an argument. Add as many as the command needs; positionals are
120 /// filled in the order they are added.
121 ///
122 /// # Examples
123 ///
124 /// ```
125 /// use cli_forge::{Arg, Command};
126 /// let cmd = Command::new("copy")
127 /// .arg(Arg::positional("from").required(true))
128 /// .arg(Arg::positional("to").required(true))
129 /// .arg(Arg::flag("force").short('f'));
130 /// ```
131 #[must_use]
132 pub fn arg(mut self, arg: Arg) -> Command {
133 self.args.push(arg);
134 self
135 }
136
137 /// Nest a subcommand. Subcommands compose recursively to any depth.
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use cli_forge::Command;
143 /// let remote = Command::new("remote")
144 /// .subcommand(Command::new("add"))
145 /// .subcommand(Command::new("remove"));
146 /// ```
147 #[must_use]
148 pub fn subcommand(mut self, cmd: Command) -> Command {
149 self.subcommands.push(cmd);
150 self
151 }
152
153 /// Hide the command from generated help while leaving it invokable.
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// use cli_forge::Command;
159 /// let cmd = Command::new("debug-dump").hidden(true);
160 /// ```
161 #[must_use]
162 pub fn hidden(mut self, yes: bool) -> Command {
163 self.hidden = yes;
164 self
165 }
166
167 /// Mark the command as requiring authentication.
168 ///
169 /// With the `auth` feature enabled, the command runs — and appears in help —
170 /// only when the app's `App::auth` hook authorizes it; otherwise invoking it
171 /// yields [`ParseError::Unauthorized`](crate::ParseError::Unauthorized).
172 /// Without the `auth` feature the flag is inert (the command runs and shows
173 /// normally).
174 ///
175 /// # Examples
176 ///
177 /// ```
178 /// use cli_forge::Command;
179 /// let cmd = Command::new("publish").requires_auth(true);
180 /// ```
181 #[must_use]
182 pub fn requires_auth(mut self, yes: bool) -> Command {
183 self.requires_auth = yes;
184 self
185 }
186
187 /// Attach the handler run when this command is selected. It receives the
188 /// [`Matches`] parsed for this command's level.
189 ///
190 /// # Examples
191 ///
192 /// ```
193 /// use cli_forge::{out, Command};
194 /// let cmd = Command::new("hello").run(|_| out("hello"));
195 /// ```
196 #[must_use]
197 pub fn run(mut self, handler: impl Fn(&Matches) + 'static) -> Command {
198 self.handler = Some(Box::new(handler));
199 self
200 }
201
202 /// Find an argument by its long form.
203 pub(crate) fn find_long(&self, long: &str) -> Option<&Arg> {
204 self.args.iter().find(|a| a.long_name() == Some(long))
205 }
206
207 /// Find an argument by its short form.
208 pub(crate) fn find_short(&self, short: char) -> Option<&Arg> {
209 self.args.iter().find(|a| a.short == Some(short))
210 }
211
212 /// Whether `name` matches this command's name or any of its aliases.
213 pub(crate) fn matches_name(&self, name: &str) -> bool {
214 self.name == name || self.aliases.iter().any(|a| a == name)
215 }
216
217 /// Find a direct subcommand by name or alias.
218 pub(crate) fn find_subcommand(&self, name: &str) -> Option<&Command> {
219 self.subcommands.iter().find(|c| c.matches_name(name))
220 }
221
222 /// The positional arguments, in declaration order.
223 pub(crate) fn positionals(&self) -> impl Iterator<Item = &Arg> {
224 self.args.iter().filter(|a| a.kind == ArgKind::Positional)
225 }
226}
227
228impl fmt::Debug for Command {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.debug_struct("Command")
231 .field("name", &self.name)
232 .field("aliases", &self.aliases)
233 .field("about", &self.about)
234 .field("args", &self.args)
235 .field("subcommands", &self.subcommands)
236 .field("hidden", &self.hidden)
237 .field("requires_auth", &self.requires_auth)
238 .field("has_handler", &self.handler.is_some())
239 .finish()
240 }
241}