diff --git a/src/build/arg/mod.rs b/src/build/arg/mod.rs index 813b097b240..9bdc0ca460a 100644 --- a/src/build/arg/mod.rs +++ b/src/build/arg/mod.rs @@ -45,6 +45,9 @@ pub use self::regex::RegexRef; type Validator<'a> = dyn FnMut(&str) -> Result<(), Box> + Send + 'a; type ValidatorOs<'a> = dyn FnMut(&OsStr) -> Result<(), Box> + Send + 'a; +type ValidatorAll<'a> = dyn FnMut(&[&str]) -> Result<(), Box> + Send + 'a; +type ValidatorAllOs<'a> = + dyn FnMut(&[&OsStr]) -> Result<(), Box> + Send + 'a; #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) enum ArgProvider { @@ -110,6 +113,8 @@ pub struct Arg<'help> { pub(crate) min_vals: Option, pub(crate) validator: Option>>>, pub(crate) validator_os: Option>>>, + pub(crate) validator_all: Option>>>, + pub(crate) validator_all_os: Option>>>, pub(crate) val_delim: Option, pub(crate) default_vals: Vec<&'help OsStr>, pub(crate) default_vals_ifs: Vec<(Id, Option<&'help OsStr>, Option<&'help OsStr>)>, @@ -2347,6 +2352,30 @@ impl<'help> Arg<'help> { self } + /// Works identically to `validator` but is passed all the values sent. + pub fn validator_all(mut self, mut f: F) -> Self + where + F: FnMut(&[&str]) -> Result + Send + 'help, + E: Into>, + { + self.validator_all = Some(Arc::new(Mutex::new(move |s: &[&str]| { + f(s).map(|_| ()).map_err(|e| e.into()) + }))); + self + } + + /// Works identically to `validator_all` but is passed all the values sent as `OsString`'s. + pub fn validator_all_os(mut self, mut f: F) -> Self + where + F: FnMut(&[&OsStr]) -> Result + Send + 'help, + E: Into>, + { + self.validator_all_os = Some(Arc::new(Mutex::new(move |s: &[&OsStr]| { + f(s).map(|_| ()).map_err(|e| e.into()) + }))); + self + } + /// Validates the argument via the given regular expression. /// /// As regular expressions are not very user friendly, the additional `err_message` should diff --git a/src/parse/errors.rs b/src/parse/errors.rs index ec3108d58bb..5aa03adda04 100644 --- a/src/parse/errors.rs +++ b/src/parse/errors.rs @@ -919,6 +919,7 @@ impl Error { arg: String, val: String, err: Box, + multiple: bool, ) -> Self { let mut err = Self::value_validation_with_color( arg, @@ -926,6 +927,7 @@ impl Error { err, app.get_color(), app.settings.is_set(AppSettings::WaitOnError), + multiple, ); match &mut err.message { Message::Raw(_) => { @@ -941,7 +943,7 @@ impl Error { val: String, err: Box, ) -> Self { - Self::value_validation_with_color(arg, val, err, ColorChoice::Never, false) + Self::value_validation_with_color(arg, val, err, ColorChoice::Never, false, false) } fn value_validation_with_color( @@ -950,11 +952,16 @@ impl Error { err: Box, color: ColorChoice, wait_on_exit: bool, + multiple: bool, ) -> Self { let mut c = Colorizer::new(true, color); start_error(&mut c, "Invalid value"); + if multiple { + c.none("s"); + } + c.none(" for '"); c.warning(arg.clone()); c.none("'"); diff --git a/src/parse/validator.rs b/src/parse/validator.rs index 051a5b43525..590afad593f 100644 --- a/src/parse/validator.rs +++ b/src/parse/validator.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + // Internal use crate::{ build::{arg::PossibleValue, AppSettings as AS, Arg, ArgSettings}, @@ -86,6 +88,43 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> { ma: &MatchedArg, matcher: &ArgMatcher, ) -> ClapResult<()> { + if let Some(ref vtor) = arg.validator_all { + debug!("Validator::validate_arg_values: checking validator_all..."); + let mut vtor = vtor.lock().unwrap(); + let vals = ma + .vals_flatten() + .map(|s| s.to_string_lossy()) + .collect::>(); + if let Err(e) = vtor(&vals.iter().map(|s| s.borrow()).collect::>()) { + debug!("error"); + return Err(Error::value_validation( + self.p.app, + arg.to_string(), + String::new(), + e, + true, + )); + } else { + debug!("good"); + } + } + if let Some(ref vtor) = arg.validator_all_os { + debug!("Validator::validate_arg_values: checking validator_all_os..."); + let mut vtor = vtor.lock().unwrap(); + let vals = ma.vals_flatten().map(|s| s.as_os_str()).collect::>(); + if let Err(e) = vtor(vals.as_slice()) { + debug!("error"); + return Err(Error::value_validation( + self.p.app, + arg.to_string(), + String::new(), + e, + true, + )); + } else { + debug!("good"); + } + } debug!("Validator::validate_arg_values: arg={:?}", arg.name); for val in ma.vals_flatten() { if !arg.is_set(ArgSettings::AllowInvalidUtf8) && val.to_str().is_none() { @@ -152,6 +191,7 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> { arg.to_string(), val.to_string_lossy().into_owned(), e, + false, )); } else { debug!("good"); @@ -167,6 +207,7 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> { arg.to_string(), val.to_string_lossy().into(), e, + false, )); } else { debug!("good"); diff --git a/tests/validators.rs b/tests/validators.rs index 7c9e71d7725..7d64d432519 100644 --- a/tests/validators.rs +++ b/tests/validators.rs @@ -64,3 +64,76 @@ fn stateful_validator() { assert!(state); } + +#[test] +fn validator_all() { + App::new("test") + .arg( + Arg::new("test") + .short('d') + .takes_value(true) + .multiple_values(true) + .validator_all(|vals| { + if vals.len() % 3 == 0 { + Ok(()) + } else { + Err("not % 3!") + } + }), + ) + .try_get_matches_from(&["app", "-d", "f", "f", "f"]) + .unwrap(); +} + +#[test] +fn validator_all_and_validator() { + let app = App::new("test").arg( + Arg::new("test") + .short('d') + .takes_value(true) + .multiple_values(true) + .validator_all(|vals| { + if vals.len() % 3 == 0 { + Ok(()) + } else { + Err("not % 3!") + } + }) + .validator(|val| val.parse::().map_err(|e| e.to_string())), + ); + + app.clone() + .try_get_matches_from(&["app", "-d", "10", "0", "10"]) + .unwrap(); + assert!(app + .try_get_matches_from(&["app", "-d", "a", "0", "10"]) + .is_err()); +} + +#[test] +fn validator_all_os_error() { + let res = App::new("test") + .arg( + Arg::new("test") + .short('d') + .takes_value(true) + .multiple_values(true) + .validator_all_os(|vals| { + if vals.len() % 3 == 0 { + Ok(()) + } else { + Err(format!("not % 3 == 0, == {}!", vals.len() % 3)) + } + }), + ) + .try_get_matches_from(&["app", "-d", "f", "f", "f", "f"]); + + assert!(res.is_err()); + let err = res.unwrap_err(); + + eprintln!("{}", err.to_string()); + + assert!(err + .to_string() + .contains("Invalid values for '-d ...': not % 3 == 0, == 1!")); +}