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}