diff --git a/src/app/settings.rs b/src/app/settings.rs index 1c08c7df307..09206631a0f 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 0e11f88779a..f2be6df2e80 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 365eaf91599..0a4ba3e34df 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 77abe981da0..96362d176c2 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 73907c1f895..8839ea5b725 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/src/macros.rs b/src/macros.rs index ba24ddde6bd..72892e01063 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -350,6 +350,226 @@ macro_rules! arg_enum { }; } +/// A convienience macro for defining enums that can be used to access `SubCommand`s. By using this +/// macro, all traits are implemented automatically. The traits implemented are, `SubCommandKey`, +/// `Display`, and `AsRef`. This macro also implements a `variants()` function which returns an +/// array of `&'static str`s containing the variant names. +/// +/// There are two ways to use this macro, in an as-is scenario where the variants one defines are +/// exaclty how the subcommands are displayed to the end user. There is also an alternative way where +/// the actual display of the subcommands can be changed. Examples of both are bellow. +/// +/// This allows rustc to do some checking for you, i.e. if you add another +/// subcommand later, but forget to check for it, rustc will complain about +/// NonExaustive matches. Likewise, if you make a simple spelling or typing +/// error. +/// +/// **NOTE:** A final variant `None` is always added to define when no subcommand was used. You +/// should not add this variant yourself. +/// +/// **Pro Tip:** It's good practice to make the name of the enum the same as +/// the parent command, and the variants the names of the actual subcommands +/// +/// # Examples +/// +/// ```rust +/// # #[macro_use] +/// # extern crate clap; +/// # use clap::{App, SubCommand}; +/// // Note lowercase variants, the subcommand will be exactly as typed here +/// subcommands!{ +/// enum MyProg { +/// show, +/// delete, +/// make +/// } +/// } +/// +/// // Alternatively, if you wish to have variants which display +/// // differently, or contain hyphens ("-") one can use this variation of +/// // the macro +/// subcommands!{ +/// enum MyProgAlt { +/// Show => "show", +/// Delete => "delete", +/// DoStuff => "do-stuff" +/// } +/// } +/// +/// fn main() { +/// let m = App::new("myprog") +/// .subcommand(SubCommand::with_name(MyProg::show)) +/// .subcommand(SubCommand::with_name(MyProg::delete)) +/// .subcommand(SubCommand::with_name(MyProg::make)) +/// .get_matches_from(vec!["myprog", "show"]); +/// +/// match m.subcommand() { +/// (MyProg::show, _) => println!("'myprog show' was used"), +/// (MyProg::delete, _) => println!("'myprog delete' was used"), +/// (MyProg::make, _) => println!("'myprog make' was used"), +/// (MyProg::None, _) => println!("No subcommand was used"), +/// } +/// } +/// ``` +#[macro_export] +macro_rules! subcommands { + (@as_item $($i:item)*) => ($($i)*); + (@impls_s ( $($tts:tt)* ) -> ($e:ident, $($v:ident=>$s:expr),+)) => { + arg_enum!(@as_item + + #[derive(PartialEq)] + #[allow(non_camel_case_types)] + $($tts)* + + impl<'a> ::clap::SubCommandKey<'a> for $e { + fn from_str(s: &'a str) -> Self { + match s { + $($s => $e::$v),+, + _ => unreachable!(), + } + } + fn none() -> Self { + $e::None + } + } + impl ::std::fmt::Display for $e { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match *self { + $($e::$v => write!(f, $s),)+ + $e::None => write!(f, "none"), + } + } + } + impl ::std::convert::AsRef for $e { + fn as_ref(&self) -> &'static str { + match *self { + $($e::$v => $s,)+ + $e::None => "none", + } + } + } + impl $e { + #[allow(dead_code)] + pub fn variants() -> [&'static str; _clap_count_exprs!($(stringify!($v)),+)] { + [ + $(stringify!($s),)+ + ] + } + }); + }; + (@impls ( $($tts:tt)* ) -> ($e:ident, $($v:ident),+)) => { + arg_enum!(@as_item + + #[derive(PartialEq)] + #[allow(non_camel_case_types)] + $($tts)* + + impl<'a> ::clap::SubCommandKey<'a> for $e { + fn from_str(s: &'a str) -> Self { + match s { + $(stringify!($v) => $e::$v),+, + _ => unreachable!(), + } + } + fn none() -> Self { + $e::None + } + } + impl ::std::fmt::Display for $e { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match *self { + $($e::$v => write!(f, stringify!($v)),)+ + $e::None => write!(f, "none"), + } + } + } + impl ::std::convert::AsRef for $e { + fn as_ref(&self) -> &'static str { + match *self { + $($e::$v => stringify!($v),)+ + $e::None => "none", + } + } + } + impl $e { + #[allow(dead_code)] + pub fn variants() -> [&'static str; _clap_count_exprs!($(stringify!($v)),+)] { + [ + $(stringify!($v),)+ + ] + } + }); + }; + (#[$($m:meta),+] pub enum $e:ident { $($v:ident=>$s:expr),+ } ) => { + subcommands!(@impls_s + (#[$($m),+] + pub enum $e { + $($v),+, + None + }) -> ($e, $($v=>$s),+) + ); + }; + (#[$($m:meta),+] enum $e:ident { $($v:ident=>$s:expr),+ } ) => { + subcommands!(@impls_s + (#[$($m),+] + enum $e { + $($v),+, + None + }) -> ($e, $($v=>$s:expr),+) + ); + }; + (pub enum $e:ident { $($v:ident=>$s:expr),+ } ) => { + subcommands!(@impls_s + (pub enum $e { + $($v),+, + None + }) -> ($e, $($v=>$s),+) + ); + }; + (enum $e:ident { $($v:ident=>$s:expr),+ } ) => { + subcommands!(@impls_s + (enum $e { + $($v),+, + None + }) -> ($e, $($v=>$s),+) + ); + }; + (#[$($m:meta),+] pub enum $e:ident { $($v:ident),+ } ) => { + subcommands!(@impls + (#[$($m),+] + pub enum $e { + $($v),+, + None + }) -> ($e, $($v),+) + ); + }; + (#[$($m:meta),+] enum $e:ident { $($v:ident),+ } ) => { + subcommands!(@impls + (#[$($m),+] + enum $e { + $($v),+, + None + }) -> ($e, $($v),+) + ); + }; + (pub enum $e:ident { $($v:ident),+ } ) => { + subcommands!(@impls + (pub enum $e { + $($v),+, + None + }) -> ($e, $($v),+) + ); + }; + (enum $e:ident { $($v:ident),+ } ) => { + subcommands!(@impls + (enum $e { + $($v),+, + None + }) -> ($e, $($v),+) + ); + }; +} + /// Allows you pull the version for an from your Cargo.toml at compile time as /// MAJOR.MINOR.PATCH_PKGVERSION_PRE /// diff --git a/tests/subcommands.rs b/tests/subcommands.rs index 2423a9d0f02..07f6e729a8c 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");