feat(parser): accept boolean literal with env vars, take 1

This commit is contained in:
rami3l 2021-07-31 13:14:38 +02:00
parent 6527fdeec4
commit f2f9b665ed
4 changed files with 134 additions and 31 deletions

View file

@ -1772,40 +1772,91 @@ impl<'help, 'app> Parser<'help, 'app> {
matcher: &mut ArgMatcher, matcher: &mut ArgMatcher,
trailing_values: bool, trailing_values: bool,
) -> ClapResult<()> { ) -> ClapResult<()> {
use crate::util::{str_to_bool, FALSE_LITERALS, TRUE_LITERALS};
if self.app.is_set(AS::DisableEnv) { if self.app.is_set(AS::DisableEnv) {
debug!("Parser::add_env: Env vars disabled, quitting");
return Ok(()); return Ok(());
} }
for a in self.app.args.args() { self.app.args.args().try_for_each(|a| {
// Use env only if the arg was not present among command line args // Use env only if the arg was absent among command line args,
if matcher.get(&a.id).map_or(true, |a| a.occurs == 0) { // early return if this is not the case.
if let Some((_, Some(ref val))) = a.env { if matcher.get(&a.id).map_or(false, |a| a.occurs != 0) {
let val = ArgStr::new(val); debug!("Parser::add_env: Skipping existing arg `{}`", a);
if a.is_set(ArgSettings::TakesValue) { return Ok(());
self.add_val_to_arg( }
a,
val, debug!("Parser::add_env: Checking arg `{}`", a);
matcher, if let Some((_, Some(ref val))) = a.env {
ValueType::EnvVariable, let val = ArgStr::new(val);
false,
trailing_values, if a.is_set(ArgSettings::TakesValue) {
); debug!(
} else { "Parser::add_env: Found an opt with value={:?}, trailing={:?}",
match self.check_for_help_and_version_str(&val) { val, trailing_values
Some(ParseResult::HelpFlag) => { );
return Err(self.help_err(true)); self.add_val_to_arg(
} a,
Some(ParseResult::VersionFlag) => { val,
return Err(self.version_err(true)); matcher,
} ValueType::EnvVariable,
_ => (), false,
trailing_values,
);
return Ok(());
}
debug!("Parser::add_env: Checking for help and version");
// Early return on `HelpFlag` or `VersionFlag`.
match self.check_for_help_and_version_str(&val) {
Some(ParseResult::HelpFlag) => {
return Err(self.help_err(true));
}
Some(ParseResult::VersionFlag) => {
return Err(self.version_err(true));
}
_ => (),
}
debug!("Parser::add_env: Found a flag with value `{:?}`", val);
match str_to_bool(val.to_string_lossy()) {
Ok(predicate) => {
debug!("Parser::add_env: Found boolean literal `{}`", predicate);
if predicate {
matcher.add_index_to(&a.id, self.cur_idx.get(), ValueType::EnvVariable);
} }
matcher.add_index_to(&a.id, self.cur_idx.get(), ValueType::EnvVariable); }
Err(rest) => {
debug!("Parser::parse_long_arg: Got invalid literal `{}`", rest);
let used: Vec<Id> = matcher
.arg_names()
.filter(|&n| {
self.app.find(n).map_or(true, |a| {
!(a.is_set(ArgSettings::Hidden)
|| self.required.contains(&a.id))
})
})
.cloned()
.collect();
let good_vals: Vec<&str> = TRUE_LITERALS
.iter()
.chain(FALSE_LITERALS.iter())
.copied()
.collect();
return Err(ClapError::invalid_value(
rest.into(),
&good_vals,
a,
Usage::new(self).create_usage_no_title(&used),
self.app.color(),
));
} }
} }
} }
}
Ok(()) Ok(())
})
} }
/// Increase occurrence of specific argument and the grouped arg it's in. /// Increase occurrence of specific argument and the grouped arg it's in.

View file

@ -4,10 +4,16 @@ mod argstr;
mod fnv; mod fnv;
mod graph; mod graph;
mod id; mod id;
mod str_to_bool;
pub use self::fnv::Key; pub use self::fnv::Key;
pub(crate) use self::{argstr::ArgStr, graph::ChildGraph, id::Id}; pub(crate) use self::{
argstr::ArgStr,
graph::ChildGraph,
id::Id,
str_to_bool::{str_to_bool, FALSE_LITERALS, TRUE_LITERALS},
};
pub(crate) use vec_map::VecMap; pub(crate) use vec_map::VecMap;
#[cfg(feature = "color")] #[cfg(feature = "color")]

24
src/util/str_to_bool.rs Normal file
View file

@ -0,0 +1,24 @@
/// True values are `y`, `yes`, `t`, `true`, `on`, and `1`.
pub(crate) const TRUE_LITERALS: [&str; 6] = ["y", "yes", "t", "true", "on", "1"];
/// False values are `n`, `no`, `f`, `false`, `off`, and `0`.
pub(crate) const FALSE_LITERALS: [&str; 6] = ["n", "no", "f", "false", "off", "0"];
/// Converts a string literal representation of truth to true or false.
///
/// Translated from the Python function [`strtobool`].
///
/// [`strtobool`]: https://docs.python.org/3/distutils/apiref.html#distutils.util.strtobool
///
/// # Errors
/// Returns Err(val) if `val` is anything else.
pub(crate) fn str_to_bool<S: AsRef<str>>(val: S) -> Result<bool, S> {
let pat: &str = &val.as_ref().to_lowercase();
if TRUE_LITERALS.contains(&pat) {
Ok(true)
} else if FALSE_LITERALS.contains(&pat) {
Ok(false)
} else {
Err(val)
}
}

View file

@ -1,7 +1,7 @@
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use clap::{App, AppSettings, Arg}; use clap::{App, AppSettings, Arg, Error as ClapError};
#[test] #[test]
fn env() { fn env() {
@ -30,11 +30,33 @@ fn env_no_takes_value() {
.arg(Arg::from("[arg] 'some opt'").env("CLP_TEST_ENV")) .arg(Arg::from("[arg] 'some opt'").env("CLP_TEST_ENV"))
.try_get_matches_from(vec![""]); .try_get_matches_from(vec![""]);
assert!(matches!(
dbg!(r),
Err(ClapError {
kind: clap::ErrorKind::InvalidValue,
..
})
));
}
#[test]
fn env_bool_literal() {
env::set_var("CLP_TEST_FLAG_TRUE", "On");
env::set_var("CLP_TEST_FLAG_FALSE", "nO");
let r = App::new("df")
.arg(Arg::from("[present] 'some opt'").env("CLP_TEST_FLAG_TRUE"))
.arg(Arg::from("[negated] 'some another opt'").env("CLP_TEST_FLAG_FALSE"))
.arg(Arg::from("[absent] 'some third opt'").env("CLP_TEST_FLAG_ABSENT"))
.try_get_matches_from(vec![""]);
assert!(r.is_ok()); assert!(r.is_ok());
let m = r.unwrap(); let m = r.unwrap();
assert!(m.is_present("arg")); assert!(m.is_present("present"));
assert_eq!(m.occurrences_of("arg"), 0); assert_eq!(m.occurrences_of("present"), 0);
assert_eq!(m.value_of("arg"), None); assert_eq!(m.value_of("present"), None);
assert!(!m.is_present("negated"));
assert!(!m.is_present("absent"));
} }
#[test] #[test]