Merge pull request #2515 from clap-rs/yaml

Ignore extra fields in YAML only when specified
This commit is contained in:
Pavan Kumar Sunkara 2021-06-01 22:44:52 +01:00 committed by GitHub
commit a8134ddcda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 263 additions and 210 deletions

View file

@ -92,6 +92,9 @@
"$ref": "#/structures/tuple3StrOptStrStr"
}
},
"default_missing_value": {
"type": "string"
},
"display_order": {
"type": "integer"
},
@ -110,9 +113,6 @@
"groups": {
"$ref": "#/structures/arrayStrUnique"
},
"help": {
"type": "string"
},
"help_heading": {
"$ref": "#/structures/optionalStr"
},
@ -128,10 +128,10 @@
"hide_default_value": {
"type": "boolean"
},
"hide_env_value": {
"hide_env_values": {
"type": "boolean"
},
"hide_possible_value": {
"hide_possible_values": {
"type": "boolean"
},
"index": {
@ -146,18 +146,12 @@
"long_about": {
"type": "string"
},
"long_help": {
"type": "string"
},
"max_values": {
"type": "integer"
},
"min_values": {
"type": "integer"
},
"multiple": {
"type": "boolean"
},
"multiple_occurences": {
"type": "boolean"
},
@ -194,22 +188,22 @@
"required": {
"type": "boolean"
},
"required_if": {
"required_if_eq": {
"$ref": "#/structures/tuple2StrStr"
},
"required_ifs": {
"required_if_eq_any": {
"type": "array",
"items": {
"$ref": "#/structures/tuple2StrStr"
}
},
"required_unless": {
"required_unless_present": {
"type": "string"
},
"required_unless_all": {
"required_unless_present_all": {
"$ref": "#/structures/arrayStrUnique"
},
"required_unless_one": {
"required_unless_present_any": {
"$ref": "#/structures/arrayStrUnique"
},
"requires": {
@ -329,9 +323,6 @@
"aliases": {
"$ref": "#/structures/arrayStrUnique"
},
"arg": {
"$ref": "#/definitions/arg"
},
"args": {
"type": "array",
"items": {
@ -356,9 +347,6 @@
"global_settings": {
"$ref": "#/structures/arrayStrUnique"
},
"group": {
"$ref": "#/definitions/argGroup"
},
"groups": {
"type": "array",
"items": {
@ -406,9 +394,6 @@
"settings": {
"$ref": "#/structures/arrayStrUnique"
},
"subcommand": {
"$ref": "#"
},
"subcommands": {
"type": "array",
"items": {
@ -416,7 +401,7 @@
}
},
"term_width": {
"type": "string"
"type": "integer"
},
"version": {
"type": "string"

View file

@ -13,22 +13,22 @@ args:
# The name of this argument, is 'opt' which will be used to access the value
# later in your Rust code
- opt:
help: example option argument from yaml
about: example option argument from yaml
short: o
long: option
multiple: true
multiple_values: true
takes_value: true
- pos:
help: example positional argument from yaml
about: example positional argument from yaml
index: 1
# A list of possible values can be defined as a list
possible_values:
- fast
- slow
- flag:
help: demo flag argument
about: demo flag argument
short: F
multiple: true
multiple_values: true
takes_value: true
global: true
# Conflicts, mutual overrides, and requirements can all be defined as a
@ -39,13 +39,13 @@ args:
- pos
- mode:
long: mode
help: shows an option with specific values
about: shows an option with specific values
# possible_values can also be defined in this list format
possible_values: [ vi, emacs ]
takes_value: true
- mvals:
long: mult-vals
help: demos an option which has two named values
about: demos an option which has two named values
# value names can be described in a list, where the help will be shown
# --mult-vals <one> <two>
value_names:
@ -53,13 +53,13 @@ args:
- two
- minvals:
long: min-vals
multiple: true
help: you must supply at least two values to satisfy me
multiple_values: true
about: you must supply at least two values to satisfy me
min_values: 2
- maxvals:
long: max-vals
multiple: true
help: you can only supply a max of 3 values for me!
multiple_values: true
about: you can only supply a max of 3 values for me!
max_values: 3
# All subcommands must be listed in the 'subcommand:' object, where the key to
@ -76,11 +76,11 @@ subcommands:
args:
- scopt:
short: B
multiple: true
help: example subcommand option
multiple_values: true
about: example subcommand option
takes_value: true
- scpos1:
help: example subcommand positional
about: example subcommand positional
index: 1
# ArgGroups are supported as well, and must be specified in the 'groups:'

View file

@ -2810,138 +2810,91 @@ impl<'help> Index<&'_ Id> for App<'help> {
#[cfg(feature = "yaml")]
impl<'help> From<&'help Yaml> for App<'help> {
#[allow(clippy::cognitive_complexity)]
fn from(mut yaml: &'help Yaml) -> Self {
fn from(y: &'help Yaml) -> Self {
// We WANT this to panic on error...so expect() is good.
let mut is_sc = None;
let mut a = if let Some(name) = yaml["name"].as_str() {
App::new(name)
let (mut a, yaml, err) = if let Some(name) = y["name"].as_str() {
(App::new(name), y, "app".into())
} else {
let yaml_hash = yaml.as_hash().unwrap();
let sc_key = yaml_hash.keys().next().unwrap();
is_sc = Some(yaml_hash.get(sc_key).unwrap());
App::new(sc_key.as_str().unwrap())
let yaml_hash = y.as_hash().unwrap();
let name_yaml = yaml_hash.keys().next().unwrap();
let name_str = name_yaml.as_str().unwrap();
(
App::new(name_str),
yaml_hash.get(name_yaml).unwrap(),
format!("subcommand '{}'", name_str),
)
};
yaml = if let Some(sc) = is_sc { sc } else { yaml };
macro_rules! yaml_str {
($a:ident, $y:ident, $i:ident) => {
if let Some(v) = $y[stringify!($i)].as_str() {
$a = $a.$i(v);
} else if $y[stringify!($i)] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to a string",
$y[stringify!($i)]
);
let mut has_metadata = false;
for (k, v) in yaml.as_hash().unwrap().iter() {
a = match k.as_str().unwrap() {
"_has_metadata" => {
has_metadata = true;
a
}
};
}
yaml_str!(a, yaml, version);
yaml_str!(a, yaml, long_version);
yaml_str!(a, yaml, author);
yaml_str!(a, yaml, bin_name);
yaml_str!(a, yaml, about);
yaml_str!(a, yaml, before_help);
yaml_str!(a, yaml, before_long_help);
yaml_str!(a, yaml, after_help);
yaml_str!(a, yaml, after_long_help);
yaml_str!(a, yaml, alias);
yaml_str!(a, yaml, visible_alias);
if let Some(v) = yaml["display_order"].as_i64() {
a = a.display_order(v as usize);
} else if yaml["display_order"] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to a u64",
yaml["display_order"]
);
}
if let Some(v) = yaml["setting"].as_str() {
a = a.setting(v.parse().expect("unknown AppSetting found in YAML file"));
} else if yaml["setting"] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to an AppSetting",
yaml["setting"]
);
}
if let Some(v) = yaml["settings"].as_vec() {
for ys in v {
if let Some(s) = ys.as_str() {
a = a.setting(s.parse().expect("unknown AppSetting found in YAML file"));
}
}
} else if let Some(v) = yaml["settings"].as_str() {
a = a.setting(v.parse().expect("unknown AppSetting found in YAML file"));
} else if yaml["settings"] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to a string",
yaml["settings"]
);
}
if let Some(v) = yaml["global_setting"].as_str() {
a = a.setting(v.parse().expect("unknown AppSetting found in YAML file"));
} else if yaml["global_setting"] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to an AppSetting",
yaml["setting"]
);
}
if let Some(v) = yaml["global_settings"].as_vec() {
for ys in v {
if let Some(s) = ys.as_str() {
a = a.global_setting(s.parse().expect("unknown AppSetting found in YAML file"));
}
}
} else if let Some(v) = yaml["global_settings"].as_str() {
a = a.global_setting(v.parse().expect("unknown AppSetting found in YAML file"));
} else if yaml["global_settings"] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to a string",
yaml["global_settings"]
);
}
macro_rules! vec_or_str {
($a:ident, $y:ident, $as_vec:ident, $as_single:ident) => {{
let maybe_vec = $y[stringify!($as_vec)].as_vec();
if let Some(vec) = maybe_vec {
for ys in vec {
if let Some(s) = ys.as_str() {
$a = $a.$as_single(s);
} else {
panic!("Failed to convert YAML value {:?} to a string", ys);
"bin_name" => yaml_to_str!(a, v, bin_name),
"version" => yaml_to_str!(a, v, version),
"long_version" => yaml_to_str!(a, v, long_version),
"author" => yaml_to_str!(a, v, author),
"about" => yaml_to_str!(a, v, about),
"before_help" => yaml_to_str!(a, v, before_help),
"before_long_help" => yaml_to_str!(a, v, before_long_help),
"after_help" => yaml_to_str!(a, v, after_help),
"after_long_help" => yaml_to_str!(a, v, after_long_help),
"help_heading" => yaml_to_str!(a, v, help_heading),
"help_template" => yaml_to_str!(a, v, help_template),
"override_help" => yaml_to_str!(a, v, override_help),
"override_usage" => yaml_to_str!(a, v, override_usage),
"alias" => yaml_to_str!(a, v, alias),
"aliases" => yaml_vec_or_str!(a, v, alias),
"visible_alias" => yaml_to_str!(a, v, visible_alias),
"visible_aliases" => yaml_vec_or_str!(a, v, visible_alias),
"display_order" => yaml_to_usize!(a, v, display_order),
"term_width" => yaml_to_usize!(a, v, term_width),
"max_term_width" => yaml_to_usize!(a, v, max_term_width),
"args" => {
if let Some(vec) = v.as_vec() {
for arg_yaml in vec {
a = a.arg(Arg::from(arg_yaml));
}
} else {
panic!("Failed to convert YAML value {:?} to a vec", v);
}
} else {
if let Some(s) = $y[stringify!($as_vec)].as_str() {
$a = $a.$as_single(s);
} else if $y[stringify!($as_vec)] != Yaml::BadValue {
panic!(
"Failed to convert YAML value {:?} to either a vec or string",
$y[stringify!($as_vec)]
);
}
a
}
"subcommands" => {
if let Some(vec) = v.as_vec() {
for sc_yaml in vec {
a = a.subcommand(App::from(sc_yaml));
}
} else {
panic!("Failed to convert YAML value {:?} to a vec", v);
}
a
}
"groups" => {
if let Some(vec) = v.as_vec() {
for ag_yaml in vec {
a = a.group(ArgGroup::from(ag_yaml));
}
} else {
panic!("Failed to convert YAML value {:?} to a vec", v);
}
a
}
"setting" | "settings" => yaml_to_setting!(a, v, setting, "AppSetting", err),
"global_setting" | "global_settings" => {
yaml_to_setting!(a, v, global_setting, "AppSetting", err)
}
"name" => continue,
s => {
if !has_metadata {
panic!("Unknown setting '{}' in YAML file for {}", s, err)
}
continue;
}
$a
}};
}
a = vec_or_str!(a, yaml, aliases, alias);
a = vec_or_str!(a, yaml, visible_aliases, visible_alias);
if let Some(v) = yaml["args"].as_vec() {
for arg_yaml in v {
a = a.arg(Arg::from(arg_yaml));
}
}
if let Some(v) = yaml["subcommands"].as_vec() {
for sc_yaml in v {
a = a.subcommand(App::from(sc_yaml));
}
}
if let Some(v) = yaml["groups"].as_vec() {
for ag_yaml in v {
a = a.group(ArgGroup::from(ag_yaml));
}
}

View file

@ -4233,7 +4233,7 @@ impl<'help> Arg<'help> {
}
}
/// Placeholder documentation
/// @TODO@ @release @docs
#[inline]
pub fn multiple_values(self, multi: bool) -> Self {
if multi {
@ -4668,23 +4668,29 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
/// ```
#[allow(clippy::cognitive_complexity)]
fn from(y: &'help Yaml) -> Self {
let y = y.as_hash().unwrap();
let yaml_hash = y.as_hash().unwrap();
// We WANT this to panic on error...so expect() is good.
let name_yaml = y.keys().next().unwrap();
let name_yaml = yaml_hash.keys().next().unwrap();
let name_str = name_yaml.as_str().unwrap();
let mut a = Arg::new(name_str);
let arg_settings = y.get(name_yaml).unwrap().as_hash().unwrap();
let yaml = yaml_hash.get(name_yaml).unwrap();
for (k, v) in arg_settings.iter() {
let mut has_metadata = false;
for (k, v) in yaml.as_hash().unwrap().iter() {
a = match k.as_str().unwrap() {
"_has_metadata" => {
has_metadata = true;
a
}
"short" => yaml_to_char!(a, v, short),
"long" => yaml_to_str!(a, v, long),
"alias" => yaml_to_str!(a, v, alias),
"aliases" => yaml_vec_or_str!(a, v, alias),
"short_alias" => yaml_to_str!(a, v, alias),
"short_aliases" => yaml_to_chars!(a, v, short_aliases),
"about" => yaml_to_str!(a, v, about),
"long_about" => yaml_to_str!(a, v, long_about),
"help" => yaml_to_str!(a, v, about),
"long_help" => yaml_to_str!(a, v, long_about),
"required" => yaml_to_bool!(a, v, required),
"required_if_eq" => yaml_tuple2!(a, v, required_if_eq),
"required_if_eq_any" => yaml_array_tuple2!(a, v, required_if_eq_any),
@ -4692,10 +4698,11 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
"takes_value" => yaml_to_bool!(a, v, takes_value),
"index" => yaml_to_usize!(a, v, index),
"global" => yaml_to_bool!(a, v, global),
"multiple" => yaml_to_bool!(a, v, multiple),
"multiple_occurrences" => yaml_to_bool!(a, v, multiple_occurrences),
"multiple_values" => yaml_to_bool!(a, v, multiple_values),
"hidden" => yaml_to_bool!(a, v, hidden),
"hidden_long_help" => yaml_to_bool!(a, v, hidden_long_help),
"hidden_short_help" => yaml_to_bool!(a, v, hidden_short_help),
"next_line_help" => yaml_to_bool!(a, v, next_line_help),
"group" => yaml_to_str!(a, v, group),
"number_of_values" => yaml_to_usize!(a, v, number_of_values),
@ -4704,8 +4711,10 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
"value_name" => yaml_to_str!(a, v, value_name),
"use_delimiter" => yaml_to_bool!(a, v, use_delimiter),
"allow_hyphen_values" => yaml_to_bool!(a, v, allow_hyphen_values),
"raw" => yaml_to_bool!(a, v, raw),
"require_equals" => yaml_to_bool!(a, v, require_equals),
"require_delimiter" => yaml_to_bool!(a, v, require_delimiter),
"value_terminator" => yaml_to_str!(a, v, value_terminator),
"value_delimiter" => yaml_to_str!(a, v, value_delimiter),
"required_unless_present" => yaml_to_str!(a, v, required_unless_present),
"display_order" => yaml_to_usize!(a, v, display_order),
@ -4721,9 +4730,14 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
"requires_ifs" => yaml_tuple2!(a, v, requires_if),
"conflicts_with" => yaml_vec_or_str!(a, v, conflicts_with),
"exclusive" => yaml_to_bool!(a, v, exclusive),
"last" => yaml_to_bool!(a, v, last),
"value_hint" => yaml_str_parse!(a, v, value_hint),
"hide_default_value" => yaml_to_bool!(a, v, hide_default_value),
"overrides_with" => yaml_vec_or_str!(a, v, overrides_with),
"hide_env_values" => yaml_to_bool!(a, v, hide_env_values),
"hide_possible_values" => yaml_to_bool!(a, v, hide_possible_values),
"overrides_with" => yaml_to_str!(a, v, overrides_with),
"overrides_with_all" => yaml_vec_or_str!(a, v, overrides_with),
"possible_value" => yaml_to_str!(a, v, possible_value),
"possible_values" => yaml_vec_or_str!(a, v, possible_value),
"case_insensitive" => yaml_to_bool!(a, v, case_insensitive),
"required_unless_present_any" => yaml_vec!(a, v, required_unless_present_any),
@ -4753,7 +4767,18 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
panic!("Failed to convert YAML value to vector")
}
}
_ => continue, // Ignore extra fields
"setting" | "settings" => {
yaml_to_setting!(a, v, setting, "ArgSetting", format!("arg '{}'", name_str))
}
s => {
if !has_metadata {
panic!(
"Unknown setting '{}' in YAML file for arg '{}'",
s, name_str
)
}
continue;
}
}
}

View file

@ -210,3 +210,30 @@ macro_rules! yaml_to_usize {
as usize)
}};
}
#[cfg(feature = "yaml")]
macro_rules! yaml_to_setting {
($a:ident, $v:ident, $c:ident, $t:literal, $n:expr) => {{
if let Some(v) = $v.as_vec() {
for ys in v {
if let Some(s) = ys.as_str() {
$a = $a.$c(s.parse().unwrap_or_else(|_| {
panic!("Unknown {} '{}' found in YAML file for {}", $t, s, $n)
}));
} else {
panic!(
"Failed to convert YAML {:?} value to an array of strings",
$v
);
}
}
} else if let Some(v) = $v.as_str() {
$a = $a.$c(v
.parse()
.unwrap_or_else(|_| panic!("Unknown {} '{}' found in YAML file for {}", $t, v, $n)))
} else {
panic!("Failed to convert YAML {:?} value to a string", $v);
}
$a
}};
}

View file

@ -4,18 +4,16 @@ about: tests clap library
author: Kevin K. <kbknapp@gmail.com>
settings:
- ArgRequiredElseHelp
ignored_field: This field is ignored
args:
- help:
short: h
long: help
about: prints help with a nonstandard description
ignored_field: This field is ignored
- option:
short: o
long: option
takes_value: true
multiple: true
multiple_values: true
about: tests options
- positional:
about: tests positionals
@ -30,7 +28,7 @@ args:
short: f
long: flag
takes_value: true
multiple: true
multiple_values: true
about: tests flags
global: true
- flag2:
@ -69,14 +67,14 @@ args:
- two
- multvalsmo:
long: multvalsmo
multiple: true
multiple_values: true
about: Tests multiple values, not mult occs
value_names: [one, two]
- multvalsdelim:
long: multvalsdelim
about: Tests multiple values with required delimiter
takes_value: true
multiple: true
multiple_occurrences: true
use_delimiter: true
require_delimiter: true
- settings:
@ -105,12 +103,12 @@ args:
short_aliases: [b, c]
- minvals2:
long: minvals2
multiple: true
multiple_values: true
about: Tests 2 min vals
min_values: 2
- maxvals3:
long: maxvals3
multiple: true
multiple_values: true
about: Tests 3 max vals
max_values: 3
- exclusive:
@ -119,18 +117,18 @@ args:
exclusive: true
- case_insensitive:
index: 4
help: Test case_insensitive
about: Test case_insensitive
possible_values: [test123, test321]
case_insensitive: true
- value_hint:
long: value-hint
help: Test value_hint
about: Test value_hint
value_hint: FilePath
- verbose:
short: v
multiple_occurrences: true
takes_value: false
help: Sets the level of verbosity
about: Sets the level of verbosity
- visiblealiases:
long: visiblealiases
about: Tests visible aliases
@ -141,12 +139,11 @@ args:
about: Tests visible short aliases
visible_short_alias: e
visible_short_aliases: [l, m]
arg_groups:
groups:
- test:
args:
- maxvals3
- minmals2
- minvals2
conflicts_with:
- option3
requires:
@ -160,7 +157,7 @@ subcommands:
- scoption:
short: o
long: option
multiple: true
multiple_values: true
about: tests options
takes_value: true
- scpositional:

View file

@ -2,16 +2,10 @@ name: claptests
version: "1.0"
about: tests clap library
author: Kevin K. <kbknapp@gmail.com>
settings:
- ArgRequiredElseHelp
args:
- help:
short: h
long: help
about: prints help with a nonstandard description
- opt:
short: o
long: option
takes_value: true
multiple: true
multiple_values: true
about: tests options

View file

@ -2,13 +2,7 @@ name: clapregextest
version: "1.0"
about: tests clap regex functionality
author: Benjamin Kästner <benjamin.kaestner@gmail.com>
settings:
- ArgRequiredElseHelp
args:
- help:
short: h
long: help
about: prints help with a nonstandard description
- filter:
index: 1
validator_regex: ["^*\\.[a-z]+$", expected extension pattern]

View file

@ -2,13 +2,7 @@ name: clapregextest
version: "1.0"
about: tests clap regex functionality
author: Benjamin Kästner <benjamin.kaestner@gmail.com>
settings:
- ArgRequiredElseHelp
args:
- help:
short: h
long: help
about: prints help with a nonstandard description
- filter:
index: 1
validator_regex: [")", invalid regular expression]

View file

@ -0,0 +1,5 @@
name: claptests
version: "1.0"
about: tests clap extra fields
settings:
- random

View file

@ -0,0 +1,8 @@
name: claptests
version: "1.0"
about: tests clap extra fields
args:
- option:
long: option
settings:
- random

10
tests/fixtures/extra_fields.yaml vendored Normal file
View file

@ -0,0 +1,10 @@
_has_metadata: true
name: claptests
version: "1.0"
about: tests clap extra fields
random: This field is extra
args:
- option:
_has_metadata: true
long: option
random: This field is extra

View file

@ -0,0 +1,6 @@
name: claptests
version: "1.0"
about: tests clap extra fields
subcommands:
- info:
random: This field is extra

View file

@ -0,0 +1,7 @@
name: claptests
version: "1.0"
about: tests clap extra fields
args:
- option:
long: option
random: This field is extra

View file

@ -1,6 +1,6 @@
#![cfg(feature = "yaml")]
use clap::{load_yaml, App, ValueHint};
use clap::{load_yaml, App, ErrorKind, ValueHint};
#[test]
fn create_app_from_yaml() {
@ -42,6 +42,34 @@ fn author() {
assert!(help_string.contains("Kevin K. <kbknapp@gmail.com>"));
}
#[test]
fn app_settings() {
let yaml = load_yaml!("fixtures/app.yaml");
let app = App::from(yaml);
let m = app.try_get_matches_from(vec!["prog"]);
assert!(m.is_err());
assert_eq!(
m.unwrap_err().kind,
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
#[should_panic = "Unknown AppSetting 'random' found in YAML file for app"]
fn app_setting_invalid() {
let yaml = load_yaml!("fixtures/app_setting_invalid.yaml");
App::from(yaml);
}
#[test]
#[should_panic = "Unknown ArgSetting 'random' found in YAML file for arg 'option'"]
fn arg_setting_invalid() {
let yaml = load_yaml!("fixtures/arg_setting_invalid.yaml");
App::from(yaml);
}
// ValueHint must be parsed correctly from Yaml
#[test]
fn value_hint() {
@ -150,5 +178,25 @@ fn regex_with_valid_string() {
#[should_panic]
fn regex_with_invalid_yaml() {
let yml = load_yaml!("fixtures/app_regex_invalid.yaml");
let _app = App::from(yml);
App::from(yml);
}
#[test]
fn extra_fields() {
let yml = load_yaml!("fixtures/extra_fields.yaml");
App::from(yml);
}
#[test]
#[should_panic = "Unknown setting 'random' in YAML file for arg 'option'"]
fn extra_fields_invalid_arg() {
let yml = load_yaml!("fixtures/extra_fields_invalid_arg.yaml");
App::from(yml);
}
#[test]
#[should_panic = "Unknown setting 'random' in YAML file for subcommand 'info'"]
fn extra_fields_invalid_app() {
let yml = load_yaml!("fixtures/extra_fields_invalid_app.yaml");
App::from(yml);
}