Skip to content

Commit 86cf4c4

Browse files
committed
feat(YAML): allows building a CLI from YAML files
1 parent f482387 commit 86cf4c4

File tree

7 files changed

+283
-41
lines changed

7 files changed

+283
-41
lines changed

src/app/app.rs

+45-41
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use std::env;
33
use std::io::{self, BufRead, Write};
44
use std::path::Path;
55

6+
#[cfg(feature = "yaml")]
7+
use yaml_rust::Yaml;
8+
69
use args::{ArgMatches, Arg, SubCommand, MatchedArg};
710
use args::{FlagBuilder, OptBuilder, PosBuilder};
811
use args::ArgGroup;
@@ -149,50 +152,51 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
149152
///
150153
/// # Example
151154
///
152-
/// ```no_run
153-
/// # use clap::{App, Arg};
154-
/// let prog = App::from_yaml(include!("my_app.yml"));
155+
/// ```ignore
156+
/// # use clap::App;
157+
/// let yml = load_yaml!("app.yml");
158+
/// let app = App::from_yaml(yml);
155159
/// ```
156160
#[cfg(feature = "yaml")]
157-
pub fn from_yaml(n: &'ar str) -> Self {
158-
159-
App {
160-
name: n.to_owned(),
161-
name_slice: n,
162-
author: None,
163-
about: None,
164-
more_help: None,
165-
version: None,
166-
flags: BTreeMap::new(),
167-
opts: BTreeMap::new(),
168-
positionals_idx: BTreeMap::new(),
169-
positionals_name: HashMap::new(),
170-
subcommands: BTreeMap::new(),
171-
needs_long_version: true,
172-
needs_long_help: true,
173-
needs_subcmd_help: true,
174-
help_short: None,
175-
version_short: None,
176-
required: vec![],
177-
short_list: vec![],
178-
long_list: vec![],
179-
usage_str: None,
180-
usage: None,
181-
blacklist: vec![],
182-
bin_name: None,
183-
groups: HashMap::new(),
184-
subcmds_neg_reqs: false,
185-
global_args: vec![],
186-
no_sc_error: false,
187-
help_str: None,
188-
wait_on_error: false,
189-
help_on_no_args: false,
190-
help_on_no_sc: false,
191-
global_ver: false,
192-
versionless_scs: None,
193-
unified_help: false,
194-
overrides: vec![]
161+
pub fn from_yaml<'y>(doc: &'y Yaml) -> App<'y, 'y, 'y, 'y, 'y, 'y> {
162+
// We WANT this to panic on error...so expect() is good.
163+
let mut a = App::new(doc["name"].as_str().unwrap());
164+
if let Some(v) = doc["version"].as_str() {
165+
a = a.version(v);
166+
}
167+
if let Some(v) = doc["author"].as_str() {
168+
a = a.author(v);
169+
}
170+
if let Some(v) = doc["bin_name"].as_str() {
171+
a = a.bin_name(v);
172+
}
173+
if let Some(v) = doc["about"].as_str() {
174+
a = a.about(v);
175+
}
176+
if let Some(v) = doc["after_help"].as_str() {
177+
a = a.after_help(v);
178+
}
179+
if let Some(v) = doc["usage"].as_str() {
180+
a = a.usage(v);
181+
}
182+
if let Some(v) = doc["help"].as_str() {
183+
a = a.help(v);
195184
}
185+
if let Some(v) = doc["help_short"].as_str() {
186+
a = a.help_short(v);
187+
}
188+
if let Some(v) = doc["version_short"].as_str() {
189+
a = a.version_short(v);
190+
}
191+
if let Some(v) = doc["settings"].as_vec() {
192+
for ys in v {
193+
if let Some(s) = ys.as_str() {
194+
a = a.setting(s.parse().ok().expect("unknown AppSetting found in YAML file"));
195+
}
196+
}
197+
}
198+
199+
a
196200
}
197201

198202
/// Sets a string of author(s) and will be displayed to the user when they request the help

src/app/settings.rs

+20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::str::FromStr;
2+
use std::ascii::AsciiExt;
3+
14
/// Application level settings, which affect how `App` operates
25
pub enum AppSettings {
36
/// Allows subcommands to override all requirements of the parent (this command). For example
@@ -136,4 +139,21 @@ pub enum AppSettings {
136139
/// # ;
137140
/// ```
138141
SubcommandRequiredElseHelp,
142+
}
143+
144+
impl FromStr for AppSettings {
145+
type Err = String;
146+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
147+
match &*s.to_ascii_lowercase() {
148+
"subcommandsnegatereqs" => Ok(AppSettings::SubcommandsNegateReqs),
149+
"subcommandsrequired" => Ok(AppSettings::SubcommandRequired),
150+
"argrequiredelsehelp" => Ok(AppSettings::ArgRequiredElseHelp),
151+
"globalversion" => Ok(AppSettings::GlobalVersion),
152+
"versionlesssubcommands" => Ok(AppSettings::VersionlessSubcommands),
153+
"unifiedhelpmessage" => Ok(AppSettings::UnifiedHelpMessage),
154+
"waitonerror" => Ok(AppSettings::WaitOnError),
155+
"subcommandrequiredelsehelp" => Ok(AppSettings::SubcommandRequiredElseHelp),
156+
_ => Err("unknown AppSetting, cannot convert from str".to_owned())
157+
}
158+
}
139159
}

src/args/arg.rs

+111
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
use std::iter::IntoIterator;
22
use std::collections::HashSet;
3+
#[cfg(feature = "yaml")]
4+
use std::collections::BTreeMap;
35
use std::rc::Rc;
46

7+
#[cfg(feature = "yaml")]
8+
use yaml_rust::Yaml;
9+
510
use usageparser::{UsageParser, UsageToken};
611

712
/// 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> {
143148
}
144149
}
145150

151+
/// Creates a new instace of `App` from a .yml (YAML) file.
152+
///
153+
/// # Example
154+
///
155+
/// ```ignore
156+
/// # use clap::App;
157+
/// let yml = load_yaml!("app.yml");
158+
/// let app = App::from_yaml(yml);
159+
/// ```
160+
#[cfg(feature = "yaml")]
161+
pub fn from_yaml<'y>(y: &'y BTreeMap<Yaml, Yaml>) -> Arg<'y, 'y, 'y, 'y, 'y, 'y> {
162+
debugln!("arg_yaml={:#?}", y);
163+
// We WANT this to panic on error...so expect() is good.
164+
let name_yml = y.keys().nth(0).unwrap();
165+
let name_str = name_yml.as_str().unwrap();
166+
let mut a = Arg::with_name(name_str);
167+
let arg_settings = y.get(name_yml).unwrap().as_hash().unwrap();
168+
169+
for (k, v) in arg_settings.iter() {
170+
a = match k.as_str().unwrap() {
171+
"short" => a.short(v.as_str().unwrap()),
172+
"long" => a.long(v.as_str().unwrap()),
173+
"help" => a.help(v.as_str().unwrap()),
174+
"required" => a.required(v.as_bool().unwrap()),
175+
"takes_value" => a.takes_value(v.as_bool().unwrap()),
176+
"index" => a.index(v.as_i64().unwrap() as u8),
177+
"global" => a.global(v.as_bool().unwrap()),
178+
"multiple" => a.multiple(v.as_bool().unwrap()),
179+
"empty_values" => a.empty_values(v.as_bool().unwrap()),
180+
"group" => a.group(v.as_str().unwrap()),
181+
"number_of_values" => a.number_of_values(v.as_i64().unwrap() as u8),
182+
"max_values" => a.max_values(v.as_i64().unwrap() as u8),
183+
"min_values" => a.min_values(v.as_i64().unwrap() as u8),
184+
"value_name" => a.value_name(v.as_str().unwrap()),
185+
"value_names" => {
186+
for ys in v.as_vec().unwrap() {
187+
if let Some(s) = ys.as_str() {
188+
a = a.value_name(s);
189+
}
190+
}
191+
a
192+
},
193+
"requires" => {
194+
for ys in v.as_vec().unwrap() {
195+
if let Some(s) = ys.as_str() {
196+
a = a.requires(s);
197+
}
198+
}
199+
a
200+
},
201+
"conflicts_with" => {
202+
for ys in v.as_vec().unwrap() {
203+
if let Some(s) = ys.as_str() {
204+
a = a.conflicts_with(s);
205+
}
206+
}
207+
a
208+
},
209+
"mutually_overrides_with" => {
210+
for ys in v.as_vec().unwrap() {
211+
if let Some(s) = ys.as_str() {
212+
a = a.mutually_overrides_with(s);
213+
}
214+
}
215+
a
216+
},
217+
"possible_values" => {
218+
for ys in v.as_vec().unwrap() {
219+
if let Some(s) = ys.as_str() {
220+
a = a.possible_value(s);
221+
}
222+
}
223+
a
224+
},
225+
s => panic!("Unknown Arg setting '{}' in YAML file for arg '{}'", s, name_str)
226+
}
227+
}
228+
229+
a
230+
}
231+
146232
/// Creates a new instace of `Arg` from a usage string. Allows creation of basic settings
147233
/// for Arg (i.e. everything except relational rules). The syntax is flexible, but there are
148234
/// some rules to follow.
@@ -675,6 +761,31 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
675761
self
676762
}
677763

764+
/// Specifies a possible value for this argument. At runtime, clap verifies that only
765+
/// one of the specified values was used, or fails with a usage string.
766+
///
767+
/// **NOTE:** This setting only applies to options and positional arguments
768+
///
769+
///
770+
/// # Example
771+
///
772+
/// ```no_run
773+
/// # use clap::{App, Arg};
774+
/// # let matches = App::new("myprog")
775+
/// # .arg(
776+
/// # Arg::with_name("debug").index(1)
777+
/// .possible_value("fast")
778+
/// .possible_value("slow")
779+
/// # ).get_matches();
780+
pub fn possible_value(mut self, name: &'p str) -> Self {
781+
if let Some(ref mut vec) = self.possible_vals {
782+
vec.push(name);
783+
} else {
784+
self.possible_vals = Some(vec![name]);
785+
}
786+
self
787+
}
788+
678789
/// Specifies the name of the group the argument belongs to.
679790
///
680791
///

src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
extern crate strsim;
1414
#[cfg(feature = "color")]
1515
extern crate ansi_term;
16+
#[cfg(feature = "yaml")]
17+
extern crate yaml_rust;
1618

19+
#[cfg(feature = "yaml")]
20+
pub use yaml_rust::YamlLoader;
1721
pub use args::{Arg, SubCommand, ArgMatches, ArgGroup};
1822
pub use app::{App, AppSettings};
1923
pub use fmt::Format;

src/macros.rs

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ macro_rules! debug {
3636
($fmt:expr, $($arg:tt)*) => ();
3737
}
3838

39+
#[cfg(feature = "yaml")]
40+
#[macro_export]
41+
macro_rules! load_yaml {
42+
($yml:expr) => (
43+
&::clap::YamlLoader::load_from_str(include_str!($yml)).ok().expect("failed to load YAML file")[0]
44+
);
45+
}
46+
3947
// convienience macro for remove an item from a vec
4048
macro_rules! vec_remove {
4149
($vec:expr, $to_rem:ident) => {

tests/app.yml

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: claptests
2+
version: 1.0
3+
about: tests clap library
4+
author: Kevin K. <[email protected]>
5+
args:
6+
- opt:
7+
short: o
8+
long: option
9+
multiple: true
10+
help: tests options
11+
- positional:
12+
help: tests positionals
13+
takes_value: true
14+
- positional2:
15+
help: tests positionals with exclusions
16+
takes_value: true
17+
- flag:
18+
short: f
19+
long: flag
20+
multiple: true
21+
help: tests flags
22+
global: true
23+
- flag2:
24+
short: F
25+
help: tests flags with exclusions
26+
conflicts_with:
27+
- flag
28+
requires:
29+
- option2
30+
- option2:
31+
long: long-option-2
32+
help: tests long options with exclusions
33+
conflicts_with:
34+
- option
35+
requires:
36+
- positional2
37+
- option3:
38+
short: O
39+
long: Option
40+
help: tests options with specific value sets
41+
takes_value: true
42+
possible_values:
43+
- fast
44+
- slow
45+
- positional3:
46+
takes_value: true
47+
help: tests positionals with specific values
48+
possible_values: [ vi, emacs ]
49+
- multvals:
50+
long: multvals
51+
help: Tests mutliple values, not mult occs
52+
value_names:
53+
- one
54+
- two
55+
- multvalsmo:
56+
long: multvalsmo
57+
multiple: true
58+
help: Tests mutliple values, not mult occs
59+
value_names: [one, two]
60+
- minvals2:
61+
long: minvals2
62+
multiple: true
63+
help: Tests 2 min vals
64+
min_values: 2
65+
- maxvals3:
66+
long: maxvals3
67+
multiple: true
68+
help: Tests 3 max vals
69+
max_values: 3
70+
subcommands:
71+
- subcmd:
72+
about: tests subcommands
73+
version: 0.1
74+
author: Kevin K. <[email protected]>
75+
args:
76+
- scoption:
77+
short: o
78+
long: option
79+
multiple: true
80+
help: tests options
81+
takes_value: true
82+
- scpositional:
83+
help: tests positionals
84+
takes_value: true

tests/yaml.rs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#[macro_use]
2+
extern crate clap;
3+
4+
use clap::App;
5+
6+
#[test]
7+
#[cfg(feature="yaml")]
8+
fn create_app_from_yaml() {
9+
let yml = load_yaml!("app.yml");
10+
App::from_yaml(yml);
11+
}

0 commit comments

Comments
 (0)