clap_action_command/lib.rs
1//! A command-map pattern for easily configuring and dispatching modular
2//! subcommands using the Clap argument parser.
3//!
4//! The first part is the [`ActionCommand`] trait, which is implemented
5//! to associate a behavior with a [`clap::Command`].
6//!
7//! ```rust
8//! # use clap::{
9//! # Arg, ArgMatches, Command,
10//! # builder::NonEmptyStringValueParser,
11//! # };
12//! # use vec1::Vec1;
13//! # use clap_action_command::{ActionCommand, get_one};
14//! static NAME_ARG: &str = "name";
15//!
16//! struct HelloWorldCommand {}
17//!
18//! impl ActionCommand for HelloWorldCommand {
19//! // This specifies the name of the ActionCommand, so hello-world
20//! // here would be invokable as my-program hello-world if it were
21//! // to be run from a binary called my-program.
22//! fn name(&self) -> &'static str {
23//! "hello-world"
24//! }
25//!
26//! // This is the command builder. The command itself is configured
27//! // with the name given previously, and here it may be configured
28//! // with additional args or aliases.
29//! fn command(&self, command: Command) -> Command {
30//! command
31//! .about("Say hello to the world")
32//! .alias("h")
33//! .arg(
34//! Arg::new(NAME_ARG)
35//! .short('n')
36//! .value_name("NAME")
37//! .required(false)
38//! .value_parser(NonEmptyStringValueParser::new())
39//! )
40//! }
41//!
42//! // The action to take when the ActionCommand is matched, given
43//! // a list of all (sub)commands for argument/flag retrieval.
44//! fn action(
45//! &self, matches: Vec1<&ArgMatches>
46//! ) -> Result<(), Box<dyn std::error::Error>> {
47//! if let Some(name_arg) =
48//! get_one::<String>(&matches, NAME_ARG) {
49//! println!("Hello, {}!", name_arg);
50//! } else {
51//! println!("Hello, world!");
52//! }
53//!
54//! Ok(())
55//! }
56//! }
57//! ```
58//!
59//! The second part is the [`CommandMap`] type, which aggregates one or
60//! more [`ActionCommand`]s and handles dispatching to them based on
61//! a [`clap::ArgMatches`].
62//!
63//! ```rust
64//! # use clap::{ArgMatches, Command, command};
65//! # use vec1::vec1;
66//! # use clap_action_command::{
67//! # ActionCommand, CommandMap, HelloWorldCommand
68//! # };
69//! #
70//! // Add commands to the map by pushing ActionCommand trait objects
71//! // onto the map
72//! let command_map = CommandMap::builder()
73//! .push(HelloWorldCommand {})
74//! .build();
75//! // Tell the relevant Clap Command about all the subcommands
76//! let command = Command::new("my-program")
77//! .subcommands(command_map.commands());
78//! let matches = command.get_matches_from(
79//! ["my-program", "hello-world"]
80//! );
81//! // Dispatch to the relevant subcommand, saving some if/else-if
82//! // chains
83//! command_map.dispatch(vec1![&matches]);
84//! ```
85
86pub use vec1;
87
88use clap::{
89 parser::{RawValues, ValueSource, ValuesRef},
90 ArgMatches, Command,
91};
92use snafu::{ResultExt, Snafu};
93use std::{any::Any, collections::HashMap, error::Error, iter::Flatten, vec::IntoIter};
94use vec1::Vec1;
95
96/// A type that encapsulates the Clap [`Command`] and associated
97/// behavior of that command when it is matched.
98///
99/// ```rust
100/// # use clap::{ArgMatches, Command};
101/// # use clap_action_command::ActionCommand;
102/// # use vec1::Vec1;
103/// #
104/// struct HelloWorldCommand {}
105///
106/// impl ActionCommand for HelloWorldCommand {
107/// fn name(&self) -> &'static str {
108/// "hello-world"
109/// }
110///
111/// fn command(&self, command: Command) -> Command {
112/// command
113/// .about("Say hello to the world")
114/// .alias("h")
115/// }
116///
117/// fn action(
118/// &self, _matches: Vec1<&ArgMatches>,
119/// ) -> Result<(), Box<dyn std::error::Error>> {
120/// println!("Hello, world!");
121/// Ok(())
122/// }
123/// }
124/// ```
125pub trait ActionCommand<T = (), E = Box<dyn Error>> {
126 /// The name of the command.
127 fn name(&self) -> &'static str;
128
129 /// The [`Command`] that describes how to match this on the command
130 /// line using Clap. `command` is already constructed using
131 /// [`Self::name`] for convenience.
132 fn command(&self, command: Command) -> Command;
133
134 /// The action to take when this command is matched on the command
135 /// line. [`CommandMap`]s may be nested, and this is represented
136 /// by the matches being returned as a list of at least one element.
137 fn action(&self, matches: Vec1<&ArgMatches>) -> Result<T, E>;
138}
139
140/// A type which has a set of [`ActionCommand`]s and can provide the
141/// Clap [`Command`] for command line arg parsing, as well as map a
142/// matched [`Command`] back to its [`ActionCommand`] for dispatch to
143/// its action function.
144///
145/// ```rust
146/// # use clap::{ArgMatches, Command, command};
147/// # use vec1::vec1;
148/// # use clap_action_command::{
149/// # ActionCommand, CommandMap, HelloWorldCommand
150/// # };
151/// #
152/// // Add commands to the map by pushing ActionCommand trait objects
153/// // onto the map
154/// let command_map = CommandMap::builder()
155/// .push(HelloWorldCommand {})
156/// .build();
157/// // Tell the relevant Clap Command about all the subcommands
158/// let command = Command::new("my-program")
159/// .subcommands(command_map.commands());
160/// let matches = command.get_matches_from(
161/// ["my-program", "hello-world"]
162/// );
163/// // Dispatch to the relevant subcommand, saving some if/else-if
164/// // chains
165/// command_map.dispatch(vec1![&matches]);
166/// ```
167///
168/// This type can be composed, for example on a subcommand with multiple
169/// subcommands of its own. See [`CommandMapActionCommand`] for a
170/// minimal example.
171pub struct CommandMap<'a, T = (), E = Box<dyn Error>> {
172 command_map: HashMap<&'static str, Box<dyn ActionCommand<T, E> + Send + Sync + 'a>>,
173}
174
175impl<'a> CommandMap<'a> {
176 /// Creates a builder type which is used to tell the [`CommandMap`]
177 /// about the [`ActionCommand`]s it will be mapping over.
178 pub fn builder() -> CommandMapBuilder<'a> {
179 CommandMapBuilder {
180 command_map: HashMap::new(),
181 }
182 }
183
184 /// The Clap [`Command`]s for this [`CommandMap`]. Use this with
185 /// [`clap::Command::subcommands`] to configure it to use this
186 /// [`CommandMap`].
187 pub fn commands(&self) -> Vec<Command> {
188 self.command_map
189 .values()
190 .map(|v| v.command(Command::new(v.name())))
191 .collect()
192 }
193
194 /// Dispatch this [`CommandMap`] using [`ArgMatches`].
195 ///
196 /// When starting from scratch simply construct a new
197 /// [`vec1::Vec1`] with a single [`clap::ArgMatches`] in it; when
198 /// nesting multiple [`CommandMap`]s it is helpful to keep the
199 /// previous subcommand stack accessible by extending the matches
200 /// vector using [`vec1::Vec1::from_vec_push`].
201 pub fn dispatch(&self, matches: Vec1<&ArgMatches>) -> Result<(), DispatchError> {
202 let local_matches = matches.last();
203 if let Some((command_name, subcommand)) = local_matches.subcommand() {
204 if let Some(action_command) = self.command_map.get(command_name) {
205 action_command
206 .action(Vec1::from_vec_push(matches.to_vec(), subcommand))
207 .with_context(|_| ActionCommandSnafu {
208 command_name: command_name.to_owned(),
209 })?;
210
211 return Ok(());
212 }
213
214 return Err(DispatchError::SubcommandNotInMap {
215 command_name: command_name.to_owned(),
216 all_commands: self
217 .command_map
218 .values()
219 .map(|action_command| action_command.name())
220 .collect(),
221 });
222 }
223
224 Err(DispatchError::NoSubcommand)
225 }
226}
227
228/// Error generated by [`CommandMap::dispatch`].
229#[derive(Debug, Snafu)]
230pub enum DispatchError {
231 /// An error originating from the execution of the [`ActionCommand`]
232 /// itself - an error in the business logic.
233 ActionCommand {
234 command_name: String,
235 source: Box<dyn std::error::Error>,
236 },
237
238 /// The [`CommandMap`] does not have an associated [`ActionCommand`]
239 /// named the same thing as the [`clap::ArgMatches`] matched
240 /// command. This may happen if additional [`clap::Command`]s have
241 /// been added beyond those present in the [`CommandMap`].
242 SubcommandNotInMap {
243 command_name: String,
244 all_commands: Vec<&'static str>,
245 },
246
247 /// The [`clap::ArgMatches`] does not have a subcommand, which means
248 /// that the [`clap::Command`] which matched this
249 /// [`clap::ArgMatches`] is the most specific. For example, if
250 /// `my-program` has a `hello-world` subcommand, but the
251 /// [`CommandMap`] returns [`DispatchError::NoSubcommand`], it means
252 /// that the program was invoked as `my-program` with no subcommand
253 /// at all.
254 NoSubcommand,
255}
256
257/// Used to fluently construct [`CommandMap`]s.
258pub struct CommandMapBuilder<'a> {
259 command_map: HashMap<&'static str, Box<dyn ActionCommand + Send + Sync + 'a>>,
260}
261
262impl<'a> CommandMapBuilder<'a> {
263 /// Add a new [`ActionCommand`] to the [`CommandMap`].
264 pub fn push(
265 mut self,
266 action_command: impl ActionCommand + Send + Sync + 'a,
267 ) -> CommandMapBuilder<'a> {
268 self.command_map
269 .insert(action_command.name(), Box::new(action_command));
270
271 self
272 }
273
274 /// Add zero or more [`ActionCommand`]s to the [`CommandMap`].
275 pub fn push_all(
276 mut self,
277 action_commands: impl IntoIterator<Item = impl ActionCommand + Send + Sync + 'a>,
278 ) -> CommandMapBuilder<'a> {
279 for action_command in action_commands {
280 self.command_map
281 .insert(action_command.name(), Box::new(action_command));
282 }
283
284 self
285 }
286
287 /// Finalize this builder and generate a [`CommandMap`].
288 pub fn build(self) -> CommandMap<'a> {
289 CommandMap {
290 command_map: self.command_map,
291 }
292 }
293}
294
295/// An [`ActionCommand`] that only composes a [`CommandMap`]'s
296/// subcommands. Any customization of the [`clap::Command`] or the
297/// behavior of [`CommandMapActionCommand::action`] will require a
298/// custom type.
299///
300/// ```rust
301/// # use clap_action_command::{
302/// # CommandMap, CommandMapActionCommand, HelloWorldCommand
303/// # };
304/// // create a new CommandMapActionCommand, including all of the
305/// // subcommands it must dispatch to.
306/// let foo = CommandMapActionCommand::new(
307/// "foo",
308/// CommandMap::builder()
309/// .push(HelloWorldCommand {})
310/// .build(),
311/// );
312/// // add that CommandMapActionCommand to its parent CommandMap, which
313/// // will automatically dispatch and route to subcommands
314/// let command_map = CommandMap::builder()
315/// .push(foo)
316/// .build();
317/// ```
318pub struct CommandMapActionCommand<'a> {
319 name: &'static str,
320 command_map: CommandMap<'a>,
321}
322
323impl<'a> CommandMapActionCommand<'a> {
324 /// Create a new [`CommandMapActionCommand`] give a name and a
325 /// [`CommandMap`].
326 pub fn new(name: &'static str, command_map: CommandMap<'a>) -> CommandMapActionCommand<'a> {
327 CommandMapActionCommand { name, command_map }
328 }
329}
330
331impl<'a> ActionCommand for CommandMapActionCommand<'a> {
332 fn name(&self) -> &'static str {
333 self.name
334 }
335
336 fn command(&self, command: clap::Command) -> clap::Command {
337 command.subcommands(self.command_map.commands())
338 }
339
340 fn action(&self, matches: Vec1<&ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
341 match self.command_map.dispatch(matches) {
342 Ok(()) => Ok(()),
343 Err(e) => Err(Box::new(e)),
344 }
345 }
346}
347
348/// Helper function for dealing with chains of [`ArgMatches`] while
349/// working in [`ActionCommand::action`] to find arguments which may
350/// have been spcified anywhere in the subcommand tree.
351///
352/// ```rust
353/// # use clap_action_command::get_many;
354/// # use clap::{Arg, Command};
355/// #
356/// # let command = Command::new("my-program")
357/// # .arg(Arg::new("my-arg").long("my-arg"))
358/// # .subcommand(
359/// # Command::new("my-subcommand")
360/// # .arg(Arg::new("my-arg").long("my-arg")));
361/// # let matches = command.get_matches_from([
362/// # "my-program",
363/// # "--my-arg",
364/// # "alpha",
365/// # "my-subcommand",
366/// # "--my-arg",
367/// # "beta",
368/// # ]);
369/// # let matches = vec![&matches, matches.subcommand().unwrap().1];
370/// #
371/// // my-program --my-arg alpha my-subcommand --my-arg beta
372/// let arg = get_many::<String>(&matches, "my-arg");
373/// assert_eq!(vec!["alpha", "beta"], arg.collect::<Vec<_>>());
374/// ```
375pub fn get_many<'a, T: Any + Clone + Send + Sync + 'static>(
376 matches: &[&'a ArgMatches],
377 id: &str,
378) -> Flatten<IntoIter<ValuesRef<'a, T>>> {
379 let mut collected_values = vec![];
380
381 for matches in matches.iter() {
382 if let Ok(Some(values)) = matches.try_get_many(id) {
383 collected_values.push(values);
384 }
385 }
386
387 collected_values.into_iter().flatten()
388}
389
390/// Helper function for dealing with chains of [`ArgMatches`] while
391/// working in [`ActionCommand::action`] to find arguments which may
392/// have been specified anywhere in the subcommand tree.
393///
394/// ```rust
395/// # use clap_action_command::get_one;
396/// # use clap::{Arg, Command};
397/// #
398/// # let command = Command::new("my-program")
399/// # .arg(Arg::new("my-arg").long("my-arg"))
400/// # .subcommand(
401/// # Command::new("my-subcommand")
402/// # .arg(Arg::new("my-arg").long("my-arg")));
403/// # let matches = command.get_matches_from([
404/// # "my-program",
405/// # "--my-arg",
406/// # "alpha",
407/// # "my-subcommand",
408/// # "--my-arg",
409/// # "beta",
410/// # ]);
411/// # let matches = vec![&matches, matches.subcommand().unwrap().1];
412/// #
413/// // my-program --my-arg alpha my-subcommand --my-arg beta
414/// let arg = get_one::<String>(&matches, "my-arg");
415/// assert_eq!("beta", arg.unwrap());
416/// ```
417///
418/// This function respects the provenance
419/// ([`clap::parser::ValueSource`]) of arguments. For example, a default
420/// or environment-sourced value will never override a value specified
421/// explicitly on the command line.
422pub fn get_one<'a, T: Any + Clone + Send + Sync + 'static>(
423 matches: &[&'a ArgMatches],
424 id: &str,
425) -> Option<&'a T> {
426 let mut best_match = None;
427 let mut best_match_specificity = ValueSource::DefaultValue;
428
429 for matches in matches.iter() {
430 let current_match = match matches.try_get_one::<T>(id) {
431 Ok(arg_match) => arg_match,
432 Err(_) => continue,
433 };
434 let current_specificity = matches.value_source(id);
435
436 if let Some(current_specificity) = current_specificity {
437 if best_match_specificity <= current_specificity {
438 best_match = current_match;
439 best_match_specificity = current_specificity;
440 }
441 }
442 }
443
444 best_match
445}
446
447/// Helper function for dealing with chains of [`ArgMatches`] while
448/// working in [`ActionCommand::action`] to find arguments which may
449/// have been specified anywhere in the subcommand tree.
450///
451/// ```rust
452/// # use clap_action_command::get_raw;
453/// # use clap::{Arg, Command};
454/// # use std::ffi::OsStr;
455/// #
456/// # let command = Command::new("my-program")
457/// # .arg(Arg::new("my-arg").long("my-arg"))
458/// # .subcommand(
459/// # Command::new("my-subcommand")
460/// # .arg(Arg::new("my-arg").long("my-arg")));
461/// # let matches = command.get_matches_from([
462/// # "my-program",
463/// # "--my-arg",
464/// # "alpha",
465/// # "my-subcommand",
466/// # "--my-arg",
467/// # "beta",
468/// # ]);
469/// # let matches = vec![&matches, matches.subcommand().unwrap().1];
470/// #
471/// // my-program --my-arg alpha my-subcommand --my-arg beta
472/// let arg = get_raw(&matches, "my-arg");
473/// assert_eq!(
474/// vec![OsStr::new("alpha"), OsStr::new("beta")],
475/// arg.collect::<Vec<_>>(),
476/// );
477/// ```
478pub fn get_raw<'a>(matches: &[&'a ArgMatches], id: &str) -> Flatten<IntoIter<RawValues<'a>>> {
479 let mut collected_values = vec![];
480
481 for matches in matches.iter() {
482 if let Ok(Some(values)) = matches.try_get_raw(id) {
483 collected_values.push(values);
484 }
485 }
486
487 collected_values.into_iter().flatten()
488}
489
490// just here to clean up some doctests
491// TODO: can this be turned off in normal builds with eg a
492// #[cfg(doctest)]?
493#[doc(hidden)]
494pub struct HelloWorldCommand {}
495
496impl ActionCommand for HelloWorldCommand {
497 fn name(&self) -> &'static str {
498 "hello-world"
499 }
500
501 fn command(&self, command: Command) -> Command {
502 command.about("Say hello to the world").alias("h")
503 }
504
505 fn action(&self, _matches: Vec1<&ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
506 println!("Hello, world!");
507 Ok(())
508 }
509}
510
511#[doc = include_str!("../README.md")]
512#[cfg(doctest)]
513struct ReadmeDoctests {}
514
515#[cfg(test)]
516mod tests {
517 use super::{get_one, ActionCommand, CommandMap, CommandMapActionCommand, DispatchError};
518 use clap::{builder::NonEmptyStringValueParser, Arg, ArgMatches, Command};
519 use std::ffi::OsString;
520 use vec1::{vec1, Vec1};
521
522 struct HelloWorldCommand {}
523
524 impl ActionCommand for HelloWorldCommand {
525 fn name(&self) -> &'static str {
526 "hello-world"
527 }
528
529 fn command(&self, command: clap::Command) -> clap::Command {
530 command.alias("h").arg(
531 Arg::new("bar")
532 .short('b')
533 .value_parser(NonEmptyStringValueParser::new())
534 .required(true),
535 )
536 }
537
538 fn action(&self, matches: Vec1<&ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
539 println!(
540 "Hello, World! My args are {{ foo: {}, bar: {} }}",
541 matches.first().get_one::<String>("foo").unwrap(),
542 matches.last().get_one::<String>("bar").unwrap(),
543 );
544 Ok(())
545 }
546 }
547
548 fn example_dispatch(
549 itr: impl IntoIterator<Item = impl Into<OsString> + Clone>,
550 ) -> Result<(), DispatchError> {
551 let base_command = Command::new("command_matching").arg(
552 Arg::new("foo")
553 .short('f')
554 .value_parser(NonEmptyStringValueParser::new())
555 .required(true),
556 );
557 let command_map_action_command = CommandMapActionCommand::new(
558 "foo",
559 CommandMap::builder().push(HelloWorldCommand {}).build(),
560 );
561 let command_map = CommandMap::builder()
562 .push(command_map_action_command)
563 .build();
564 let base_command = base_command
565 .subcommands(command_map.commands())
566 .subcommand(Command::new("bar"));
567 let matches = base_command.get_matches_from(itr);
568
569 command_map.dispatch(vec1![&matches])
570 }
571
572 // --------------------------------------------------------------
573 // these tests are too large for the CommandMap doctest, and they
574 // generally focus on failure modes or subtleties not appropriate
575 // for the brevity of docs
576 // --------------------------------------------------------------
577
578 #[test]
579 fn alias_matching() {
580 let r = example_dispatch([
581 "command_matching",
582 "-f",
583 "my_foo",
584 "foo",
585 "h", // use an alias for a command rather than its full name
586 "-b",
587 "my_bar",
588 ]);
589
590 assert!(r.is_ok());
591 }
592
593 #[test]
594 fn subcommand_not_in_map() {
595 let r = example_dispatch(["command_matching", "-f", "my_foo", "bar"]);
596
597 assert!(matches!(
598 r,
599 Err(DispatchError::SubcommandNotInMap {
600 command_name: _,
601 all_commands: _,
602 })
603 ));
604 }
605
606 #[test]
607 fn no_subcommand() {
608 let r = example_dispatch(["command_matching", "-f", "my_foo"]);
609
610 assert!(matches!(r, Err(DispatchError::NoSubcommand)));
611 }
612
613 #[test]
614 fn get_one_picks_most_specific() {
615 let command = Command::new("my-program")
616 .arg(Arg::new("my-arg").long("my-arg").default_value("gamma"))
617 .subcommand(Command::new("my-subcommand").arg(Arg::new("my-arg").long("my-arg")));
618 let matches = command.get_matches_from([
619 "my-program",
620 "--my-arg",
621 "alpha",
622 "my-subcommand",
623 "--my-arg",
624 "beta",
625 ]);
626 let command_matches = vec![&matches, matches.subcommand().unwrap().1];
627 let arg = get_one::<String>(&command_matches, "my-arg");
628
629 assert_eq!("beta", arg.unwrap());
630 }
631
632 #[test]
633 fn get_one_ignores_defaults() {
634 let command = Command::new("my-program")
635 .arg(Arg::new("my-arg").long("my-arg").default_value("gamma"))
636 .subcommand(Command::new("my-subcommand").arg(Arg::new("my-arg").long("my-arg")));
637 let matches = command.get_matches_from([
638 "my-program",
639 // --my-arg defaults to gamma
640 "my-subcommand",
641 "--my-arg",
642 "beta",
643 ]);
644 let command_matches = vec![&matches, matches.subcommand().unwrap().1];
645 let arg = get_one::<String>(&command_matches, "my-arg");
646
647 assert_eq!("beta", arg.unwrap());
648 }
649}