imp(validator): Case-insensitive required_if_eq when arg is case-insensitive

This commit is contained in:
Eldritch Cheese 2021-04-25 11:19:16 -07:00
parent a4b9bfc97b
commit fa991754d3
5 changed files with 102 additions and 7 deletions

View file

@ -1460,6 +1460,38 @@ impl<'help> Arg<'help> {
/// // We did use --other=special so "cfg" had become required but was missing. /// // We did use --other=special so "cfg" had become required but was missing.
/// assert!(res.is_err()); /// assert!(res.is_err());
/// assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument); /// assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
///
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .takes_value(true)
/// .required_if_eq("other", "special")
/// .long("config"))
/// .arg(Arg::new("other")
/// .long("other")
/// .takes_value(true))
/// .try_get_matches_from(vec![
/// "prog", "--other", "SPECIAL"
/// ]);
///
/// // By default, the comparison is case-sensitive, so "cfg" wasn't required
/// assert!(res.is_ok());
///
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .takes_value(true)
/// .required_if_eq("other", "special")
/// .long("config"))
/// .arg(Arg::new("other")
/// .long("other")
/// .case_insensitive(true)
/// .takes_value(true))
/// .try_get_matches_from(vec![
/// "prog", "--other", "SPECIAL"
/// ]);
///
/// // However, case-insensitive comparisons can be enabled. This typically occurs when using Arg::possible_values().
/// assert!(res.is_err());
/// assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
/// ``` /// ```
/// [`Arg::requires(name)`]: Arg::requires() /// [`Arg::requires(name)`]: Arg::requires()
/// [Conflicting]: Arg::conflicts_with() /// [Conflicting]: Arg::conflicts_with()
@ -3700,8 +3732,15 @@ impl<'help> Arg<'help> {
} }
} }
/// When used with [`Arg::possible_values`] it allows the argument value to pass validation even /// When used with [`Arg::possible_values`] it allows the argument
/// if the case differs from that of the specified `possible_value`. /// value to pass validation even if the case differs from that of
/// the specified `possible_value`.
///
/// When other arguments are conditionally required based on the
/// value of a case-insensitive argument, the equality check done
/// by [`Arg::required_if_eq`], [`Arg::required_if_eq_any`], or
/// [`Arg::required_if_eq_all`] is case-insensitive.
///
/// ///
/// **NOTE:** Setting this requires [`ArgSettings::TakesValue`] /// **NOTE:** Setting this requires [`ArgSettings::TakesValue`]
/// ///

View file

@ -117,10 +117,11 @@ impl ArgMatcher {
self.0.args.iter() self.0.args.iter()
} }
pub(crate) fn inc_occurrence_of(&mut self, arg: &Id) { pub(crate) fn inc_occurrence_of(&mut self, arg: &Id, ci: bool) {
debug!("ArgMatcher::inc_occurrence_of: arg={:?}", arg); debug!("ArgMatcher::inc_occurrence_of: arg={:?}", arg);
let ma = self.entry(arg).or_insert(MatchedArg::new()); let ma = self.entry(arg).or_insert(MatchedArg::new());
ma.set_ty(ValueType::CommandLine); ma.set_ty(ValueType::CommandLine);
ma.set_case_insensitive(ci);
ma.occurs += 1; ma.occurs += 1;
} }

View file

@ -22,6 +22,7 @@ pub(crate) struct MatchedArg {
pub(crate) ty: ValueType, pub(crate) ty: ValueType,
indices: Vec<usize>, indices: Vec<usize>,
vals: Vec<Vec<OsString>>, vals: Vec<Vec<OsString>>,
case_insensitive: bool,
} }
impl Default for MatchedArg { impl Default for MatchedArg {
@ -37,6 +38,7 @@ impl MatchedArg {
ty: ValueType::Unknown, ty: ValueType::Unknown,
indices: Vec::new(), indices: Vec::new(),
vals: Vec::new(), vals: Vec::new(),
case_insensitive: false,
} }
} }
@ -122,13 +124,25 @@ impl MatchedArg {
} }
pub(crate) fn contains_val(&self, val: &str) -> bool { pub(crate) fn contains_val(&self, val: &str) -> bool {
self.vals_flatten() self.vals_flatten().any(|v| {
.any(|v| OsString::as_os_str(v) == OsStr::new(val)) if self.case_insensitive {
// For rust v1.53.0 and above, can use
// OsString.eq_ignore_ascii_case
// (https://github.com/rust-lang/rust/pull/80193)
v.to_string_lossy().to_lowercase() == val.to_lowercase()
} else {
OsString::as_os_str(v) == OsStr::new(val)
}
})
} }
pub(crate) fn set_ty(&mut self, ty: ValueType) { pub(crate) fn set_ty(&mut self, ty: ValueType) {
self.ty = ty; self.ty = ty;
} }
pub(crate) fn set_case_insensitive(&mut self, ci: bool) {
self.case_insensitive = ci;
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1521,10 +1521,10 @@ impl<'help, 'app> Parser<'help, 'app> {
/// Increase occurrence of specific argument and the grouped arg it's in. /// Increase occurrence of specific argument and the grouped arg it's in.
fn inc_occurrence_of_arg(&self, matcher: &mut ArgMatcher, arg: &Arg<'help>) { fn inc_occurrence_of_arg(&self, matcher: &mut ArgMatcher, arg: &Arg<'help>) {
matcher.inc_occurrence_of(&arg.id); matcher.inc_occurrence_of(&arg.id, arg.is_set(ArgSettings::IgnoreCase));
// Increment or create the group "args" // Increment or create the group "args"
for group in self.app.groups_for_arg(&arg.id) { for group in self.app.groups_for_arg(&arg.id) {
matcher.inc_occurrence_of(&group); matcher.inc_occurrence_of(&group, false);
} }
} }
} }

View file

@ -533,6 +533,47 @@ fn required_if_val_present_fail() {
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument); assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
} }
#[test]
fn required_if_val_present_case_insensitive_pass() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq("extra", "Val")
.takes_value(true)
.long("config"),
)
.arg(
Arg::new("extra")
.takes_value(true)
.long("extra")
.case_insensitive(true),
)
.try_get_matches_from(vec!["ri", "--extra", "vaL", "--config", "my.cfg"]);
assert!(res.is_ok());
}
#[test]
fn required_if_val_present_case_insensitive_fail() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq("extra", "Val")
.takes_value(true)
.long("config"),
)
.arg(
Arg::new("extra")
.takes_value(true)
.long("extra")
.case_insensitive(true),
)
.try_get_matches_from(vec!["ri", "--extra", "vaL"]);
assert!(res.is_err());
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
}
#[test] #[test]
fn required_if_all_values_present_pass() { fn required_if_all_values_present_pass() {
let res = App::new("ri") let res = App::new("ri")