Merge pull request #2360 from ldm0/allow_empty

Allow empty value by default, introduce NoEquals Error
This commit is contained in:
Pavan Kumar Sunkara 2021-03-11 14:44:41 +05:30 committed by GitHub
commit 5618ec39e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 41 deletions

View file

@ -67,7 +67,7 @@ fn assert_app_flags(arg: &Arg) {
} }
} }
checker!(AllowEmptyValues requires TakesValue); checker!(NoEmptyValues requires TakesValue);
checker!(RequireDelimiter requires TakesValue | UseValueDelimiter); checker!(RequireDelimiter requires TakesValue | UseValueDelimiter);
checker!(HidePossibleValues requires TakesValue); checker!(HidePossibleValues requires TakesValue);
checker!(AllowHyphenValues requires TakesValue); checker!(AllowHyphenValues requires TakesValue);

View file

@ -4160,10 +4160,11 @@ impl<'help> Arg<'help> {
self.multiple_occurrences(multi).multiple_values(multi) self.multiple_occurrences(multi).multiple_values(multi)
} }
/// Allows an argument to accept explicitly empty values. An empty value must be specified at /// Don't allow an argument to accept explicitly empty values. An empty value
/// the command line with an explicit `""`, `''`, or `--option=` /// must be specified at the command line with an explicit `""`, `''`, or
/// `--option=`
/// ///
/// **NOTE:** By default empty values are *not* allowed /// **NOTE:** By default empty values are allowed.
/// ///
/// **NOTE:** Setting this requires [`ArgSettings::TakesValue`]. /// **NOTE:** Setting this requires [`ArgSettings::TakesValue`].
/// ///
@ -4173,11 +4174,12 @@ impl<'help> Arg<'help> {
/// # use clap::{App, Arg, ArgSettings}; /// # use clap::{App, Arg, ArgSettings};
/// Arg::new("file") /// Arg::new("file")
/// .long("file") /// .long("file")
/// .setting(ArgSettings::TakesValue) /// .takes_value(true)
/// .setting(ArgSettings::AllowEmptyValues) /// .forbid_empty_values(true)
/// # ; /// # ;
/// ``` /// ```
/// The default is to *not* allow empty values. ///
/// The default is allowing empty values.
/// ///
/// ```rust /// ```rust
/// # use clap::{App, Arg, ErrorKind, ArgSettings}; /// # use clap::{App, Arg, ErrorKind, ArgSettings};
@ -4185,24 +4187,7 @@ impl<'help> Arg<'help> {
/// .arg(Arg::new("cfg") /// .arg(Arg::new("cfg")
/// .long("config") /// .long("config")
/// .short('v') /// .short('v')
/// .setting(ArgSettings::TakesValue)) /// .takes_value(true))
/// .try_get_matches_from(vec![
/// "prog", "--config="
/// ]);
///
/// assert!(res.is_err());
/// assert_eq!(res.unwrap_err().kind, ErrorKind::EmptyValue);
/// ```
/// By adding this setting, we can allow empty values
///
/// ```rust
/// # use clap::{App, Arg, ArgSettings};
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .long("config")
/// .short('v')
/// .setting(ArgSettings::TakesValue)
/// .setting(ArgSettings::AllowEmptyValues))
/// .try_get_matches_from(vec![ /// .try_get_matches_from(vec![
/// "prog", "--config=" /// "prog", "--config="
/// ]); /// ]);
@ -4210,8 +4195,36 @@ impl<'help> Arg<'help> {
/// assert!(res.is_ok()); /// assert!(res.is_ok());
/// assert_eq!(res.unwrap().value_of("config"), None); /// assert_eq!(res.unwrap().value_of("config"), None);
/// ``` /// ```
///
/// By adding this setting, we can forbid empty values.
///
/// ```rust
/// # use clap::{App, Arg, ErrorKind, ArgSettings};
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .long("config")
/// .short('v')
/// .takes_value(true)
/// .forbid_empty_values(true))
/// .try_get_matches_from(vec![
/// "prog", "--config="
/// ]);
///
/// assert!(res.is_err());
/// assert_eq!(res.unwrap_err().kind, ErrorKind::EmptyValue);
/// ```
/// [`ArgSettings::TakesValue`]: ./enum.ArgSettings.html#variant.TakesValue /// [`ArgSettings::TakesValue`]: ./enum.ArgSettings.html#variant.TakesValue
#[inline] #[inline]
pub fn forbid_empty_values(self, empty: bool) -> Self {
if empty {
self.setting(ArgSettings::NoEmptyValues)
} else {
self.unset_setting(ArgSettings::NoEmptyValues)
}
}
/// Placeholder documentation
#[inline]
pub fn multiple_values(self, multi: bool) -> Self { pub fn multiple_values(self, multi: bool) -> Self {
if multi { if multi {
self.setting(ArgSettings::MultipleValues) self.setting(ArgSettings::MultipleValues)

View file

@ -8,7 +8,7 @@ bitflags! {
struct Flags: u32 { struct Flags: u32 {
const REQUIRED = 1; const REQUIRED = 1;
const MULTIPLE_OCC = 1 << 1; const MULTIPLE_OCC = 1 << 1;
const EMPTY_VALS = 1 << 2; const NO_EMPTY_VALS = 1 << 2;
const GLOBAL = 1 << 3; const GLOBAL = 1 << 3;
const HIDDEN = 1 << 4; const HIDDEN = 1 << 4;
const TAKES_VAL = 1 << 5; const TAKES_VAL = 1 << 5;
@ -39,7 +39,7 @@ impl_settings! { ArgSettings, ArgFlags,
Required("required") => Flags::REQUIRED, Required("required") => Flags::REQUIRED,
MultipleOccurrences("multipleoccurrences") => Flags::MULTIPLE_OCC, MultipleOccurrences("multipleoccurrences") => Flags::MULTIPLE_OCC,
MultipleValues("multiplevalues") => Flags::MULTIPLE_VALS, MultipleValues("multiplevalues") => Flags::MULTIPLE_VALS,
AllowEmptyValues("allowemptyvalues") => Flags::EMPTY_VALS, NoEmptyValues("noemptyvalues") => Flags::NO_EMPTY_VALS,
Hidden("hidden") => Flags::HIDDEN, Hidden("hidden") => Flags::HIDDEN,
TakesValue("takesvalue") => Flags::TAKES_VAL, TakesValue("takesvalue") => Flags::TAKES_VAL,
UseValueDelimiter("usevaluedelimiter") => Flags::USE_DELIM, UseValueDelimiter("usevaluedelimiter") => Flags::USE_DELIM,
@ -80,8 +80,8 @@ pub enum ArgSettings {
MultipleValues, MultipleValues,
/// Allows an arg to appear multiple times /// Allows an arg to appear multiple times
MultipleOccurrences, MultipleOccurrences,
/// Allows an arg accept empty values such as `""` /// Disallows an arg from accepting empty values such as `""`
AllowEmptyValues, NoEmptyValues,
/// Hides an arg from the help message /// Hides an arg from the help message
Hidden, Hidden,
/// Allows an argument to take a value (such as `--option value`) /// Allows an argument to take a value (such as `--option value`)
@ -130,8 +130,8 @@ mod test {
ArgSettings::AllowHyphenValues ArgSettings::AllowHyphenValues
); );
assert_eq!( assert_eq!(
"allowemptyvalues".parse::<ArgSettings>().unwrap(), "noemptyvalues".parse::<ArgSettings>().unwrap(),
ArgSettings::AllowEmptyValues ArgSettings::NoEmptyValues
); );
assert_eq!( assert_eq!(
"hidepossiblevalues".parse::<ArgSettings>().unwrap(), "hidepossiblevalues".parse::<ArgSettings>().unwrap(),

View file

@ -116,6 +116,7 @@ pub enum ErrorKind {
/// let res = App::new("prog") /// let res = App::new("prog")
/// .arg(Arg::new("color") /// .arg(Arg::new("color")
/// .setting(ArgSettings::TakesValue) /// .setting(ArgSettings::TakesValue)
/// .setting(ArgSettings::NoEmptyValues)
/// .long("color")) /// .long("color"))
/// .try_get_matches_from(vec!["prog", "--color="]); /// .try_get_matches_from(vec!["prog", "--color="]);
/// assert!(res.is_err()); /// assert!(res.is_err());

View file

@ -1153,7 +1153,7 @@ impl<'help, 'app> Parser<'help, 'app> {
// has_eq: --flag=value // has_eq: --flag=value
let mut has_eq = false; let mut has_eq = false;
let no_val = val.is_none(); let no_val = val.is_none();
let empty_vals = opt.is_set(ArgSettings::AllowEmptyValues); let no_empty_vals = opt.is_set(ArgSettings::NoEmptyValues);
let min_vals_zero = opt.min_vals.unwrap_or(1) == 0; let min_vals_zero = opt.min_vals.unwrap_or(1) == 0;
let require_equals = opt.is_set(ArgSettings::RequireEquals); let require_equals = opt.is_set(ArgSettings::RequireEquals);
@ -1161,7 +1161,7 @@ impl<'help, 'app> Parser<'help, 'app> {
if let Some(fv) = val { if let Some(fv) = val {
has_eq = fv.starts_with("="); has_eq = fv.starts_with("=");
let v = fv.trim_start_n_matches(1, b'='); let v = fv.trim_start_n_matches(1, b'=');
if !empty_vals && (v.is_empty() || (require_equals && !has_eq)) { if no_empty_vals && (v.is_empty() || (require_equals && !has_eq)) {
debug!("Found Empty - Error"); debug!("Found Empty - Error");
return Err(ClapError::empty_value( return Err(ClapError::empty_value(
opt, opt,

View file

@ -125,9 +125,7 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
)); ));
} }
} }
if !arg.is_set(ArgSettings::AllowEmptyValues) if arg.is_set(ArgSettings::NoEmptyValues) && val.is_empty() && matcher.contains(&arg.id)
&& val.is_empty()
&& matcher.contains(&arg.id)
{ {
debug!("Validator::validate_arg_values: illegal empty val found"); debug!("Validator::validate_arg_values: illegal empty val found");
return Err(Error::empty_value( return Err(Error::empty_value(

View file

@ -14,7 +14,11 @@ fn opts() {
#[test] #[test]
fn opt_without_value_fail() { fn opt_without_value_fail() {
let r = App::new("df") let r = App::new("df")
.arg(Arg::from("-o [opt] 'some opt'").default_value("default")) .arg(
Arg::from("-o [opt] 'some opt'")
.default_value("default")
.forbid_empty_values(true),
)
.try_get_matches_from(vec!["", "-o"]); .try_get_matches_from(vec!["", "-o"]);
assert!(r.is_err()); assert!(r.is_err());
let err = r.unwrap_err(); let err = r.unwrap_err();

129
tests/empty_values.rs Normal file
View file

@ -0,0 +1,129 @@
mod utils;
use clap::{App, Arg, ArgSettings, ErrorKind};
#[test]
fn empty_values() {
let m = App::new("config")
.arg(Arg::new("config").long("config").takes_value(true))
.get_matches_from(&["config", "--config", ""]);
assert_eq!(m.value_of("config"), Some(""));
}
#[test]
fn empty_values_with_equals() {
let m = App::new("config")
.arg(Arg::new("config").long("config").takes_value(true))
.get_matches_from(&["config", "--config="]);
assert_eq!(m.value_of("config"), Some(""));
let m = App::new("config")
.arg(Arg::new("config").short('c').takes_value(true))
.get_matches_from(&["config", "-c="]);
assert_eq!(m.value_of("config"), Some(""))
}
#[test]
fn no_empty_values() {
let m = App::new("config")
.arg(
Arg::new("config")
.long("config")
.takes_value(true)
.forbid_empty_values(true),
)
.try_get_matches_from(&["config", "--config", ""]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::EmptyValue);
let m = App::new("config")
.arg(
Arg::new("config")
.short('c')
.takes_value(true)
.forbid_empty_values(true),
)
.try_get_matches_from(&["config", "-c", ""]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::EmptyValue)
}
#[test]
fn no_empty_values_with_equals() {
let m = App::new("config")
.arg(
Arg::new("config")
.long("config")
.takes_value(true)
.setting(ArgSettings::NoEmptyValues),
)
.try_get_matches_from(&["config", "--config="]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::EmptyValue);
let m = App::new("config")
.arg(
Arg::new("config")
.short('c')
.takes_value(true)
.setting(ArgSettings::NoEmptyValues),
)
.try_get_matches_from(&["config", "-c="]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::EmptyValue);
}
#[test]
fn no_empty_values_without_equals() {
let m = App::new("config")
.arg(
Arg::new("config")
.long("config")
.takes_value(true)
.setting(ArgSettings::NoEmptyValues),
)
.try_get_matches_from(&["config", "--config"]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::EmptyValue);
let m = App::new("config")
.arg(
Arg::new("config")
.short('c')
.takes_value(true)
.setting(ArgSettings::NoEmptyValues),
)
.try_get_matches_from(&["config", "-c"]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::EmptyValue)
}
#[test]
fn no_empty_values_without_equals_but_requires_equals() {
let app = App::new("config").arg(
Arg::new("config")
.long("config")
.takes_value(true)
.setting(ArgSettings::NoEmptyValues)
.setting(ArgSettings::RequireEquals),
);
let m = app.clone().try_get_matches_from(&["config", "--config"]);
// Should error on no equals rather than empty value.
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::NoEquals);
static NO_EUQALS_ERROR: &str =
"error: Equal sign is needed when assigning values to '--config=<config>'.
USAGE:
config [OPTIONS]
For more information try --help";
assert!(utils::compare_output(
app,
"config --config",
NO_EUQALS_ERROR,
true
));
}

View file

@ -34,6 +34,7 @@ fn require_equals_fail() {
.arg( .arg(
Arg::new("cfg") Arg::new("cfg")
.setting(ArgSettings::RequireEquals) .setting(ArgSettings::RequireEquals)
.setting(ArgSettings::NoEmptyValues)
.setting(ArgSettings::TakesValue) .setting(ArgSettings::TakesValue)
.long("config"), .long("config"),
) )
@ -104,6 +105,7 @@ fn require_equals_no_empty_values_fail() {
Arg::new("cfg") Arg::new("cfg")
.setting(ArgSettings::TakesValue) .setting(ArgSettings::TakesValue)
.setting(ArgSettings::RequireEquals) .setting(ArgSettings::RequireEquals)
.setting(ArgSettings::NoEmptyValues)
.long("config"), .long("config"),
) )
.arg(Arg::new("some")) .arg(Arg::new("some"))
@ -119,7 +121,6 @@ fn require_equals_empty_vals_pass() {
Arg::new("cfg") Arg::new("cfg")
.setting(ArgSettings::TakesValue) .setting(ArgSettings::TakesValue)
.setting(ArgSettings::RequireEquals) .setting(ArgSettings::RequireEquals)
.setting(ArgSettings::AllowEmptyValues)
.long("config"), .long("config"),
) )
.try_get_matches_from(vec!["prog", "--config="]); .try_get_matches_from(vec!["prog", "--config="]);
@ -422,8 +423,10 @@ fn did_you_mean() {
fn issue_665() { fn issue_665() {
let res = App::new("tester") let res = App::new("tester")
.arg("-v, --reroll-count=[N] 'Mark the patch series as PATCH vN'") .arg("-v, --reroll-count=[N] 'Mark the patch series as PATCH vN'")
.arg(Arg::from( .arg(
"--subject-prefix [Subject-Prefix] 'Use [Subject-Prefix] instead of the standard [PATCH] prefix'") ) Arg::from("--subject-prefix [Subject-Prefix] 'Use [Subject-Prefix] instead of the standard [PATCH] prefix'")
.setting(ArgSettings::NoEmptyValues)
)
.try_get_matches_from(vec!["test", "--subject-prefix", "-v", "2"]); .try_get_matches_from(vec!["test", "--subject-prefix", "-v", "2"]);
assert!(res.is_err()); assert!(res.is_err());
@ -449,7 +452,7 @@ fn issue_1047_min_zero_vals_default_val() {
fn issue_1105_setup(argv: Vec<&'static str>) -> Result<ArgMatches, clap::Error> { fn issue_1105_setup(argv: Vec<&'static str>) -> Result<ArgMatches, clap::Error> {
App::new("opts") App::new("opts")
.arg(Arg::from("-o, --option [opt] 'some option'").setting(ArgSettings::AllowEmptyValues)) .arg(Arg::from("-o, --option [opt] 'some option'"))
.arg(Arg::from("--flag 'some flag'")) .arg(Arg::from("--flag 'some flag'"))
.try_get_matches_from(argv) .try_get_matches_from(argv)
} }