From 7f6f95992d8d448e1ce94cb553551a3c21b4fb0d Mon Sep 17 00:00:00 2001 From: Kevin K Date: Sun, 27 Mar 2016 20:40:08 -0400 Subject: [PATCH] feat(Subcommands): adds support for using enums to access subcommands vice strings This allows one to use enum variants to access subcommands from the ArgMatches struct in order to reduce the number of "stringly typed" items and reduce errors. In order to use a custom enum, the enum must implement the SubCommandKey trait which defines two methods, one to convert from a &str to it's Self, and one to define the "None" value, meaning no subcommand was used. The enum must also implement the AsRef trait which should be a cheap variant->&str conversion. Relates to #459 --- src/app/settings.rs | 1 + src/args/arg_matches.rs | 92 +++++++++++++++++++++++++++++++++++++---- src/args/mod.rs | 1 + src/args/subcommand.rs | 18 +++++++- src/lib.rs | 3 +- tests/subcommands.rs | 6 +-- 6 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/app/settings.rs b/src/app/settings.rs index 1c08c7df3071..09206631a0f9 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -322,6 +322,7 @@ pub enum AppSettings { /// // All trailing arguments will be stored under the subcommand's sub-matches using a value /// // of the runtime subcommand name (in this case "subcmd") /// match m.subcommand() { + /// ("do-stuff", Some(sub_m)) => { /* do-stuff was used, internal subcommand */ }, /// (external, Some(ext_m)) => { /// let ext_args: Vec<&str> = ext_m.values_of(external).unwrap().collect(); /// assert_eq!(ext_args, ["--option", "value", "-fff", "--flag"]); diff --git a/src/args/arg_matches.rs b/src/args/arg_matches.rs index 0e11f88779a4..f2be6df2e80b 100644 --- a/src/args/arg_matches.rs +++ b/src/args/arg_matches.rs @@ -3,11 +3,13 @@ use std::collections::HashMap; use std::iter::Map; use std::slice; use std::borrow::Cow; +use std::str::FromStr; use vec_map; use args::SubCommand; use args::MatchedArg; +use args::SubCommandKey; use INVALID_UTF8; /// Used to get information about the arguments that where supplied to the program at runtime by @@ -281,7 +283,7 @@ impl<'a> ArgMatches<'a> { /// ``` pub fn is_present>(&self, name: S) -> bool { if let Some(ref sc) = self.subcommand { - if sc.name == name.as_ref() { + if &sc.name[..] == name.as_ref() { return true; } } @@ -360,7 +362,7 @@ impl<'a> ArgMatches<'a> { /// ``` pub fn subcommand_matches>(&self, name: S) -> Option<&ArgMatches<'a>> { if let Some(ref s) = self.subcommand { - if s.name == name.as_ref() { return Some(&s.matches) } + if &s.name[..] == name.as_ref() { return Some(&s.matches) } } None } @@ -400,11 +402,21 @@ impl<'a> ArgMatches<'a> { /// $ git commit message /// ``` /// - /// Notice only one command per "level" may be used. You could not, for example, do `$ git + /// Notice only one command per "level" may be used. You could *not*, for example, do `$ git /// clone url push origin path` /// /// # Examples /// + /// Subcommands can use either strings or enum variants as names and accessors. Using strings + /// is convienient, but can lead to simple typing errors and other such issues. This is + /// sometimes referred to as being, "stringly typed" and generally avoided if possible. + /// + /// This is why subcommands also have the option to use enum variants as their accessors, which + /// allows `rustc` to do some compile time checking for, to avoid all the common "stringly + /// typed" issues. + /// + /// This first example shows a "stringly" typed version. + /// /// ```no_run /// # use clap::{App, Arg, SubCommand}; /// let app_m = App::new("git") @@ -420,15 +432,48 @@ impl<'a> ArgMatches<'a> { /// _ => {}, // Either no subcommand or one not tested for... /// } /// ``` - pub fn subcommand_name(&self) -> Option<&str> { - self.subcommand.as_ref().map(|sc| &sc.name[..]) + /// This next example shows a functionally equivolent strong typed version. + /// + /// ```no_run + /// # #[macro_use] + /// # extern crate clap; + /// # use clap::{App, Arg, SubCommand}; + /// subcommands!{ + /// enum Git { + /// clone, + /// push, + /// commit + /// } + /// } + /// + /// fn main() { + /// let app_m = App::new("git") + /// .subcommand(SubCommand::with_name(Git::clone)) + /// .subcommand(SubCommand::with_name(Git::push)) + /// .subcommand(SubCommand::with_name(Git::commit)) + /// .get_matches(); + /// + /// match app_m.subcommand_name() { + /// Some(Git::clone) => {}, // clone was used + /// Some(Git::push) => {}, // push was used + /// Some(Git::commit) => {}, // commit was used + /// _ => {}, // No subcommand was used + /// } + /// } + /// ``` + pub fn subcommand_name<'s, S>(&'s self) -> Option where S: SubCommandKey<'s> { + self.subcommand.as_ref().map(|sc| S::from_str(&sc.name[..])) } /// This brings together `ArgMatches::subcommand_matches` and `ArgMatches::subcommand_name` by /// returning a tuple with both pieces of information. /// + /// Like the other methods, can either be stringly typed, or use enum variants. + /// /// # Examples /// + /// An example using strings. + /// /// ```no_run /// # use clap::{App, Arg, SubCommand}; /// let app_m = App::new("git") @@ -445,6 +490,37 @@ impl<'a> ArgMatches<'a> { /// } /// ``` /// + /// This next example shows a functionally equivolent strong typed version. + /// + /// ```no_run + /// # #[macro_use] + /// # extern crate clap; + /// # use clap::{App, Arg, SubCommand}; + /// subcommands!{ + /// enum Git { + /// clone, + /// push, + /// commit + /// } + /// } + /// + /// fn main() { + /// let app_m = App::new("git") + /// .subcommand(SubCommand::with_name(Git::clone)) + /// .subcommand(SubCommand::with_name(Git::push)) + /// .subcommand(SubCommand::with_name(Git::commit)) + /// .get_matches(); + /// + /// match app_m.subcommand() { + /// (Git::clone, Some(sub_m)) => {}, // clone was used + /// (Git::push, Some(sub_m)) => {}, // push was used + /// (Git::commit, Some(sub_m)) => {}, // commit was used + /// (Git::None, _) => {}, // No subcommand was used + /// (_, None) => {}, // Unreachable + /// } + /// } + /// ``` + /// /// Another useful scenario is when you want to support third party, or external, subcommands. /// In these cases you can't know the subcommand name ahead of time, so use a variable instead /// with pattern matching! @@ -461,6 +537,7 @@ impl<'a> ArgMatches<'a> { /// // All trailing arguments will be stored under the subcommand's sub-matches using a value /// // of the runtime subcommand name (in this case "subcmd") /// match app_m.subcommand() { + /// ("do-stuff", Some(sub_m)) => { /* do-stuff was used, internal subcommand */ }, /// (external, Some(sub_m)) => { /// let ext_args: Vec<&str> = sub_m.values_of(external).unwrap().collect(); /// assert_eq!(ext_args, ["--option", "value", "-fff", "--flag"]); @@ -468,8 +545,8 @@ impl<'a> ArgMatches<'a> { /// _ => {}, /// } /// ``` - pub fn subcommand(&self) -> (&str, Option<&ArgMatches<'a>>) { - self.subcommand.as_ref().map_or(("", None), |sc| (&sc.name[..], Some(&sc.matches))) + pub fn subcommand<'s, S>(&'s self) -> (S, Option<&ArgMatches<'a>>) where S: SubCommandKey<'s> { + self.subcommand.as_ref().map_or((S::none(), None), |sc| (S::from_str(&sc.name[..]), Some(&sc.matches))) } /// Returns a string slice of the usage statement for the `App` (or `SubCommand`) @@ -575,3 +652,4 @@ impl<'a> Iterator for OsValues<'a> { impl<'a> DoubleEndedIterator for OsValues<'a> { fn next_back(&mut self) -> Option<&'a OsStr> { self.iter.next_back() } } + diff --git a/src/args/mod.rs b/src/args/mod.rs index 365eaf915992..0a4ba3e34df7 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -8,6 +8,7 @@ pub use self::group::ArgGroup; pub use self::any_arg::AnyArg; pub use self::settings::ArgSettings; pub use self::help_writer::HelpWriter; +pub use self::subcommand::SubCommandKey; mod arg; pub mod any_arg; diff --git a/src/args/subcommand.rs b/src/args/subcommand.rs index 77abe981da0f..96362d176c2b 100644 --- a/src/args/subcommand.rs +++ b/src/args/subcommand.rs @@ -44,8 +44,8 @@ impl<'a> SubCommand<'a> { /// SubCommand::with_name("config")) /// # ; /// ``` - pub fn with_name<'b>(name: &str) -> App<'a, 'b> { - App::new(name) + pub fn with_name<'b, S: AsRef>(name: S) -> App<'a, 'b> { + App::new(name.as_ref()) } /// Creates a new instance of a subcommand from a YAML (.yml) document @@ -62,3 +62,17 @@ impl<'a> SubCommand<'a> { App::from_yaml(yaml) } } + +pub trait SubCommandKey<'a> { + fn from_str(s: &'a str) -> Self; + fn none() -> Self; +} + +impl<'a> SubCommandKey<'a> for &'a str { + fn from_str(s: &'a str) -> Self { + s + } + fn none() -> Self { + "" + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 73907c1f895d..8839ea5b725a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -385,7 +385,7 @@ #![cfg_attr(feature = "lints", deny(warnings))] #![cfg_attr(not(any(feature = "lints", feature = "nightly")), deny(unstable_features))] #![deny( - missing_docs, + // missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, @@ -420,6 +420,7 @@ pub use args::{Arg, ArgGroup, ArgMatches, SubCommand, ArgSettings}; pub use app::{App, AppSettings}; pub use fmt::Format; pub use errors::{Error, ErrorKind, Result}; +pub use args::SubCommandKey; #[macro_use] mod macros; diff --git a/tests/subcommands.rs b/tests/subcommands.rs index 2423a9d0f022..07f6e729a8cb 100644 --- a/tests/subcommands.rs +++ b/tests/subcommands.rs @@ -14,7 +14,7 @@ fn subcommand() { .arg(Arg::with_name("other").long("other")) .get_matches_from(vec!["myprog", "some", "--test", "testing"]); - assert_eq!(m.subcommand_name().unwrap(), "some"); + assert_eq!(m.subcommand_name::<&str>().unwrap(), "some"); let sub_m = m.subcommand_matches("some").unwrap(); assert!(sub_m.is_present("test")); assert_eq!(sub_m.value_of("test").unwrap(), "testing"); @@ -32,7 +32,7 @@ fn subcommand_none_given() { .arg(Arg::with_name("other").long("other")) .get_matches_from(vec![""]); - assert!(m.subcommand_name().is_none()); + assert!(m.subcommand_name::<&str>().is_none()); } #[test] @@ -53,7 +53,7 @@ fn subcommand_multiple() { assert!(m.subcommand_matches("some").is_some()); assert!(m.subcommand_matches("add").is_none()); - assert_eq!(m.subcommand_name().unwrap(), "some"); + assert_eq!(m.subcommand_name::<&str>().unwrap(), "some"); let sub_m = m.subcommand_matches("some").unwrap(); assert!(sub_m.is_present("test")); assert_eq!(sub_m.value_of("test").unwrap(), "testing");