Skip to main content

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}