Skip to content

Commit

Permalink
feat(YAML): allows building a CLI from YAML files
Browse files Browse the repository at this point in the history
  • Loading branch information
kbknapp committed Sep 1, 2015
1 parent f482387 commit 86cf4c4
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 41 deletions.
86 changes: 45 additions & 41 deletions src/app/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/app/settings.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -136,4 +139,21 @@ pub enum AppSettings {
/// # ;
/// ```
SubcommandRequiredElseHelp,
}

impl FromStr for AppSettings {
type Err = String;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::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())
}
}
}
111 changes: 111 additions & 0 deletions src/args/arg.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Yaml, Yaml>) -> 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.
Expand Down Expand Up @@ -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.
///
///
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
84 changes: 84 additions & 0 deletions tests/app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: claptests
version: 1.0
about: tests clap library
author: Kevin K. <[email protected]>
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. <[email protected]>
args:
- scoption:
short: o
long: option
multiple: true
help: tests options
takes_value: true
- scpositional:
help: tests positionals
takes_value: true
11 changes: 11 additions & 0 deletions tests/yaml.rs
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit 86cf4c4

Please sign in to comment.