Skip to content

Commit

Permalink
feat(Subcommands): adds support for using enums to access subcommands…
Browse files Browse the repository at this point in the history
… 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<str> trait which should be
a cheap variant->&str conversion.

Relates to #459
  • Loading branch information
kbknapp committed Mar 28, 2016
1 parent 72ccf0c commit b1617f6
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/app/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
92 changes: 85 additions & 7 deletions src/args/arg_matches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -281,7 +283,7 @@ impl<'a> ArgMatches<'a> {
/// ```
pub fn is_present<S: AsRef<str>>(&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;
}
}
Expand Down Expand Up @@ -360,7 +362,7 @@ impl<'a> ArgMatches<'a> {
/// ```
pub fn subcommand_matches<S: AsRef<str>>(&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
}
Expand Down Expand Up @@ -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")
Expand All @@ -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<S> 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")
Expand All @@ -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!
Expand All @@ -461,15 +537,16 @@ 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"]);
/// },
/// _ => {},
/// }
/// ```
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`)
Expand Down Expand Up @@ -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() }
}

1 change: 1 addition & 0 deletions src/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 16 additions & 2 deletions src/args/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>>(name: S) -> App<'a, 'b> {
App::new(name.as_ref())
}

/// Creates a new instance of a subcommand from a YAML (.yml) document
Expand All @@ -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 {
""
}
}
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions tests/subcommands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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]
Expand All @@ -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");
Expand Down

0 comments on commit b1617f6

Please sign in to comment.