From 86cf4c45626a36b8115446952f9069f73c1debc3 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Sun, 30 Aug 2015 23:26:17 -0400 Subject: [PATCH] feat(YAML): allows building a CLI from YAML files --- src/app/app.rs | 86 ++++++++++++++++++---------------- src/app/settings.rs | 20 ++++++++ src/args/arg.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ src/macros.rs | 8 ++++ tests/app.yml | 84 +++++++++++++++++++++++++++++++++ tests/yaml.rs | 11 +++++ 7 files changed, 283 insertions(+), 41 deletions(-) create mode 100644 tests/app.yml create mode 100644 tests/yaml.rs diff --git a/src/app/app.rs b/src/app/app.rs index 81f1ef8fac5..ca94a3f92a0 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -3,6 +3,9 @@ use std::env; use std::io::{self, BufRead, Write}; use std::path::Path; +#[cfg(feature = "yaml")] +use yaml_rust::Yaml; + use args::{ArgMatches, Arg, SubCommand, MatchedArg}; use args::{FlagBuilder, OptBuilder, PosBuilder}; use args::ArgGroup; @@ -149,50 +152,51 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{ /// /// # Example /// - /// ```no_run - /// # use clap::{App, Arg}; - /// let prog = App::from_yaml(include!("my_app.yml")); + /// ```ignore + /// # use clap::App; + /// let yml = load_yaml!("app.yml"); + /// let app = App::from_yaml(yml); /// ``` #[cfg(feature = "yaml")] - pub fn from_yaml(n: &'ar str) -> Self { - - App { - name: n.to_owned(), - name_slice: n, - author: None, - about: None, - more_help: None, - version: None, - flags: BTreeMap::new(), - opts: BTreeMap::new(), - positionals_idx: BTreeMap::new(), - positionals_name: HashMap::new(), - subcommands: BTreeMap::new(), - needs_long_version: true, - needs_long_help: true, - needs_subcmd_help: true, - help_short: None, - version_short: None, - required: vec![], - short_list: vec![], - long_list: vec![], - usage_str: None, - usage: None, - blacklist: vec![], - bin_name: None, - groups: HashMap::new(), - subcmds_neg_reqs: false, - global_args: vec![], - no_sc_error: false, - help_str: None, - wait_on_error: false, - help_on_no_args: false, - help_on_no_sc: false, - global_ver: false, - versionless_scs: None, - unified_help: false, - overrides: vec![] + pub fn from_yaml<'y>(doc: &'y Yaml) -> App<'y, 'y, 'y, 'y, 'y, 'y> { + // We WANT this to panic on error...so expect() is good. + let mut a = App::new(doc["name"].as_str().unwrap()); + if let Some(v) = doc["version"].as_str() { + a = a.version(v); + } + if let Some(v) = doc["author"].as_str() { + a = a.author(v); + } + if let Some(v) = doc["bin_name"].as_str() { + a = a.bin_name(v); + } + if let Some(v) = doc["about"].as_str() { + a = a.about(v); + } + if let Some(v) = doc["after_help"].as_str() { + a = a.after_help(v); + } + if let Some(v) = doc["usage"].as_str() { + a = a.usage(v); + } + if let Some(v) = doc["help"].as_str() { + a = a.help(v); } + if let Some(v) = doc["help_short"].as_str() { + a = a.help_short(v); + } + if let Some(v) = doc["version_short"].as_str() { + a = a.version_short(v); + } + if let Some(v) = doc["settings"].as_vec() { + for ys in v { + if let Some(s) = ys.as_str() { + a = a.setting(s.parse().ok().expect("unknown AppSetting found in YAML file")); + } + } + } + + a } /// Sets a string of author(s) and will be displayed to the user when they request the help diff --git a/src/app/settings.rs b/src/app/settings.rs index 445d3e46ff5..5ca2a5f7a72 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -1,3 +1,6 @@ +use std::str::FromStr; +use std::ascii::AsciiExt; + /// Application level settings, which affect how `App` operates pub enum AppSettings { /// Allows subcommands to override all requirements of the parent (this command). For example @@ -136,4 +139,21 @@ pub enum AppSettings { /// # ; /// ``` SubcommandRequiredElseHelp, +} + +impl FromStr for AppSettings { + type Err = String; + fn from_str(s: &str) -> Result::Err> { + match &*s.to_ascii_lowercase() { + "subcommandsnegatereqs" => Ok(AppSettings::SubcommandsNegateReqs), + "subcommandsrequired" => Ok(AppSettings::SubcommandRequired), + "argrequiredelsehelp" => Ok(AppSettings::ArgRequiredElseHelp), + "globalversion" => Ok(AppSettings::GlobalVersion), + "versionlesssubcommands" => Ok(AppSettings::VersionlessSubcommands), + "unifiedhelpmessage" => Ok(AppSettings::UnifiedHelpMessage), + "waitonerror" => Ok(AppSettings::WaitOnError), + "subcommandrequiredelsehelp" => Ok(AppSettings::SubcommandRequiredElseHelp), + _ => Err("unknown AppSetting, cannot convert from str".to_owned()) + } + } } \ No newline at end of file diff --git a/src/args/arg.rs b/src/args/arg.rs index 16f4213061e..e36ea814731 100644 --- a/src/args/arg.rs +++ b/src/args/arg.rs @@ -1,7 +1,12 @@ use std::iter::IntoIterator; use std::collections::HashSet; +#[cfg(feature = "yaml")] +use std::collections::BTreeMap; use std::rc::Rc; +#[cfg(feature = "yaml")] +use yaml_rust::Yaml; + use usageparser::{UsageParser, UsageToken}; /// The abstract representation of a command line argument used by the consumer of the library. @@ -143,6 +148,87 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> { } } + /// Creates a new instace of `App` from a .yml (YAML) file. + /// + /// # Example + /// + /// ```ignore + /// # use clap::App; + /// let yml = load_yaml!("app.yml"); + /// let app = App::from_yaml(yml); + /// ``` + #[cfg(feature = "yaml")] + pub fn from_yaml<'y>(y: &'y BTreeMap) -> Arg<'y, 'y, 'y, 'y, 'y, 'y> { + debugln!("arg_yaml={:#?}", y); + // We WANT this to panic on error...so expect() is good. + let name_yml = y.keys().nth(0).unwrap(); + let name_str = name_yml.as_str().unwrap(); + let mut a = Arg::with_name(name_str); + let arg_settings = y.get(name_yml).unwrap().as_hash().unwrap(); + + for (k, v) in arg_settings.iter() { + a = match k.as_str().unwrap() { + "short" => a.short(v.as_str().unwrap()), + "long" => a.long(v.as_str().unwrap()), + "help" => a.help(v.as_str().unwrap()), + "required" => a.required(v.as_bool().unwrap()), + "takes_value" => a.takes_value(v.as_bool().unwrap()), + "index" => a.index(v.as_i64().unwrap() as u8), + "global" => a.global(v.as_bool().unwrap()), + "multiple" => a.multiple(v.as_bool().unwrap()), + "empty_values" => a.empty_values(v.as_bool().unwrap()), + "group" => a.group(v.as_str().unwrap()), + "number_of_values" => a.number_of_values(v.as_i64().unwrap() as u8), + "max_values" => a.max_values(v.as_i64().unwrap() as u8), + "min_values" => a.min_values(v.as_i64().unwrap() as u8), + "value_name" => a.value_name(v.as_str().unwrap()), + "value_names" => { + for ys in v.as_vec().unwrap() { + if let Some(s) = ys.as_str() { + a = a.value_name(s); + } + } + a + }, + "requires" => { + for ys in v.as_vec().unwrap() { + if let Some(s) = ys.as_str() { + a = a.requires(s); + } + } + a + }, + "conflicts_with" => { + for ys in v.as_vec().unwrap() { + if let Some(s) = ys.as_str() { + a = a.conflicts_with(s); + } + } + a + }, + "mutually_overrides_with" => { + for ys in v.as_vec().unwrap() { + if let Some(s) = ys.as_str() { + a = a.mutually_overrides_with(s); + } + } + a + }, + "possible_values" => { + for ys in v.as_vec().unwrap() { + if let Some(s) = ys.as_str() { + a = a.possible_value(s); + } + } + a + }, + s => panic!("Unknown Arg setting '{}' in YAML file for arg '{}'", s, name_str) + } + } + + a + } + /// Creates a new instace of `Arg` from a usage string. Allows creation of basic settings /// for Arg (i.e. everything except relational rules). The syntax is flexible, but there are /// some rules to follow. @@ -675,6 +761,31 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> { self } + /// Specifies a possible value for this argument. At runtime, clap verifies that only + /// one of the specified values was used, or fails with a usage string. + /// + /// **NOTE:** This setting only applies to options and positional arguments + /// + /// + /// # Example + /// + /// ```no_run + /// # use clap::{App, Arg}; + /// # let matches = App::new("myprog") + /// # .arg( + /// # Arg::with_name("debug").index(1) + /// .possible_value("fast") + /// .possible_value("slow") + /// # ).get_matches(); + pub fn possible_value(mut self, name: &'p str) -> Self { + if let Some(ref mut vec) = self.possible_vals { + vec.push(name); + } else { + self.possible_vals = Some(vec![name]); + } + self + } + /// Specifies the name of the group the argument belongs to. /// /// diff --git a/src/lib.rs b/src/lib.rs index 260a0dbe48f..5511a9aee43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,11 @@ extern crate strsim; #[cfg(feature = "color")] extern crate ansi_term; +#[cfg(feature = "yaml")] +extern crate yaml_rust; +#[cfg(feature = "yaml")] +pub use yaml_rust::YamlLoader; pub use args::{Arg, SubCommand, ArgMatches, ArgGroup}; pub use app::{App, AppSettings}; pub use fmt::Format; diff --git a/src/macros.rs b/src/macros.rs index a90b2cd5d8d..4e383eb6218 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -36,6 +36,14 @@ macro_rules! debug { ($fmt:expr, $($arg:tt)*) => (); } +#[cfg(feature = "yaml")] +#[macro_export] +macro_rules! load_yaml { + ($yml:expr) => ( + &::clap::YamlLoader::load_from_str(include_str!($yml)).ok().expect("failed to load YAML file")[0] + ); +} + // convienience macro for remove an item from a vec macro_rules! vec_remove { ($vec:expr, $to_rem:ident) => { diff --git a/tests/app.yml b/tests/app.yml new file mode 100644 index 00000000000..c887d3e8f59 --- /dev/null +++ b/tests/app.yml @@ -0,0 +1,84 @@ +name: claptests +version: 1.0 +about: tests clap library +author: Kevin K. +args: + - opt: + short: o + long: option + multiple: true + help: tests options + - positional: + help: tests positionals + takes_value: true + - positional2: + help: tests positionals with exclusions + takes_value: true + - flag: + short: f + long: flag + multiple: true + help: tests flags + global: true + - flag2: + short: F + help: tests flags with exclusions + conflicts_with: + - flag + requires: + - option2 + - option2: + long: long-option-2 + help: tests long options with exclusions + conflicts_with: + - option + requires: + - positional2 + - option3: + short: O + long: Option + help: tests options with specific value sets + takes_value: true + possible_values: + - fast + - slow + - positional3: + takes_value: true + help: tests positionals with specific values + possible_values: [ vi, emacs ] + - multvals: + long: multvals + help: Tests mutliple values, not mult occs + value_names: + - one + - two + - multvalsmo: + long: multvalsmo + multiple: true + help: Tests mutliple values, not mult occs + value_names: [one, two] + - minvals2: + long: minvals2 + multiple: true + help: Tests 2 min vals + min_values: 2 + - maxvals3: + long: maxvals3 + multiple: true + help: Tests 3 max vals + max_values: 3 +subcommands: + - subcmd: + about: tests subcommands + version: 0.1 + author: Kevin K. + args: + - scoption: + short: o + long: option + multiple: true + help: tests options + takes_value: true + - scpositional: + help: tests positionals + takes_value: true diff --git a/tests/yaml.rs b/tests/yaml.rs new file mode 100644 index 00000000000..4c7f06e7bee --- /dev/null +++ b/tests/yaml.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate clap; + +use clap::App; + +#[test] +#[cfg(feature="yaml")] +fn create_app_from_yaml() { + let yml = load_yaml!("app.yml"); + App::from_yaml(yml); +} \ No newline at end of file