Merge pull request #3774 from epage/action

feat(builder): Expose ArgAction
This commit is contained in:
Ed Page 2022-05-31 21:25:19 -05:00 committed by GitHub
commit 20ed49a535
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 199 additions and 23 deletions

View file

@ -1,8 +1,135 @@
/// Behavior of arguments when they are encountered while parsing /// Behavior of arguments when they are encountered while parsing
#[derive(Clone, Debug, PartialEq, Eq)] ///
pub(crate) enum ArgAction { /// # Examples
///
/// ```rust
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .arg(
/// Arg::new("special-help")
/// .short('?')
/// .action(clap::builder::ArgAction::Help)
/// );
///
/// // Existing help still exists
/// let err = cmd.clone().try_get_matches_from(["mycmd", "-h"]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
///
/// // New help available
/// let err = cmd.try_get_matches_from(["mycmd", "-?"]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
/// ```
#[derive(Clone, Debug)]
#[non_exhaustive]
#[allow(missing_copy_implementations)] // In the future, we may accept `Box<dyn ...>`
pub enum ArgAction {
/// When encountered, store the associated value(s) in [`ArgMatches`][crate::ArgMatches]
///
/// # Examples
///
/// ```rust
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .arg(
/// Arg::new("flag")
/// .long("flag")
/// .action(clap::builder::ArgAction::StoreValue)
/// );
///
/// let matches = cmd.try_get_matches_from(["mycmd", "--flag", "value"]).unwrap();
/// assert!(matches.is_present("flag"));
/// assert_eq!(matches.occurrences_of("flag"), 1);
/// assert_eq!(
/// matches.get_many::<String>("flag").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),
/// vec!["value"]
/// );
/// ```
StoreValue, StoreValue,
Flag, /// When encountered, increment [`ArgMatches::occurrences_of`][crate::ArgMatches::occurrences_of]
///
/// No value is allowed
///
/// # Examples
///
/// ```rust
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .arg(
/// Arg::new("flag")
/// .long("flag")
/// .multiple_occurrences(true)
/// .action(clap::builder::ArgAction::IncOccurrence)
/// );
///
/// let matches = cmd.try_get_matches_from(["mycmd", "--flag", "--flag"]).unwrap();
/// assert!(matches.is_present("flag"));
/// assert_eq!(matches.occurrences_of("flag"), 2);
/// assert_eq!(matches.get_many::<String>("flag").unwrap_or_default().count(), 0);
/// ```
IncOccurrence,
/// When encountered, display [`Command::print_help`][super::App::print_help]
///
/// Depending on the flag, [`Command::print_long_help`][super::App::print_long_help] may be shown
///
/// # Examples
///
/// ```rust
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .arg(
/// Arg::new("special-help")
/// .short('?')
/// .action(clap::builder::ArgAction::Help)
/// );
///
/// // Existing help still exists
/// let err = cmd.clone().try_get_matches_from(["mycmd", "-h"]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
///
/// // New help available
/// let err = cmd.try_get_matches_from(["mycmd", "-?"]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
/// ```
Help, Help,
/// When encountered, display [`Command::version`][super::App::version]
///
/// Depending on the flag, [`Command::long_version`][super::App::long_version] may be shown
///
/// # Examples
///
/// ```rust
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .version("1.0.0")
/// .arg(
/// Arg::new("special-version")
/// .long("special-version")
/// .action(clap::builder::ArgAction::Version)
/// );
///
/// // Existing help still exists
/// let err = cmd.clone().try_get_matches_from(["mycmd", "--version"]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
///
/// // New help available
/// let err = cmd.try_get_matches_from(["mycmd", "--special-version"]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
/// ```
Version, Version,
} }
impl ArgAction {
pub(crate) fn takes_value(&self) -> bool {
match self {
Self::StoreValue => true,
Self::IncOccurrence => false,
Self::Help => false,
Self::Version => false,
}
}
}

View file

@ -1003,6 +1003,35 @@ impl<'help> Arg<'help> {
} }
} }
/// Specify the behavior when parsing an argument
///
/// # Examples
///
/// ```rust
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .arg(
/// Arg::new("flag")
/// .long("flag")
/// .action(clap::builder::ArgAction::StoreValue)
/// );
///
/// let matches = cmd.try_get_matches_from(["mycmd", "--flag", "value"]).unwrap();
/// assert!(matches.is_present("flag"));
/// assert_eq!(matches.occurrences_of("flag"), 1);
/// assert_eq!(
/// matches.get_many::<String>("flag").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),
/// vec!["value"]
/// );
/// ```
#[inline]
#[must_use]
pub fn action(mut self, action: ArgAction) -> Self {
self.action = Some(action);
self
}
/// Specify the type of the argument. /// Specify the type of the argument.
/// ///
/// This allows parsing and validating a value before storing it into /// This allows parsing and validating a value before storing it into
@ -4531,7 +4560,7 @@ impl<'help> Arg<'help> {
} }
/// Behavior when parsing the argument /// Behavior when parsing the argument
pub(crate) fn get_action(&self) -> &super::ArgAction { pub fn get_action(&self) -> &super::ArgAction {
const DEFAULT: super::ArgAction = super::ArgAction::StoreValue; const DEFAULT: super::ArgAction = super::ArgAction::StoreValue;
self.action.as_ref().unwrap_or(&DEFAULT) self.action.as_ref().unwrap_or(&DEFAULT)
} }
@ -4862,6 +4891,13 @@ impl<'help> Arg<'help> {
if self.is_positional() { if self.is_positional() {
self.settings.set(ArgSettings::TakesValue); self.settings.set(ArgSettings::TakesValue);
} }
if let Some(action) = self.action.as_ref() {
if action.takes_value() {
self.settings.set(ArgSettings::TakesValue);
} else {
self.settings.unset(ArgSettings::TakesValue);
}
}
if self.value_parser.is_none() { if self.value_parser.is_none() {
if self.is_allow_invalid_utf8_set() { if self.is_allow_invalid_utf8_set() {

View file

@ -4159,7 +4159,7 @@ impl<'help> App<'help> {
let action = super::ArgAction::StoreValue; let action = super::ArgAction::StoreValue;
a.action = Some(action); a.action = Some(action);
} else { } else {
let action = super::ArgAction::Flag; let action = super::ArgAction::IncOccurrence;
a.action = Some(action); a.action = Some(action);
} }
} }

View file

@ -642,6 +642,14 @@ fn assert_arg(arg: &Arg) {
arg.name, arg.name,
); );
assert_eq!(
arg.get_action().takes_value(),
arg.is_takes_value_set(),
"Argument `{}`'s selected action {:?} contradicts `takes_value`",
arg.name,
arg.get_action()
);
if arg.get_value_hint() != ValueHint::Unknown { if arg.get_value_hint() != ValueHint::Unknown {
assert!( assert!(
arg.is_takes_value_set(), arg.is_takes_value_set(),
@ -664,6 +672,11 @@ fn assert_arg(arg: &Arg) {
"Argument '{}' is a positional argument and can't have short or long name versions", "Argument '{}' is a positional argument and can't have short or long name versions",
arg.name arg.name
); );
assert!(
arg.is_takes_value_set(),
"Argument '{}` is positional, it must take a value",
arg.name
);
} }
if arg.is_required_set() { if arg.is_required_set() {

View file

@ -24,6 +24,7 @@ mod debug_asserts;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use action::ArgAction;
pub use app_settings::{AppFlags, AppSettings}; pub use app_settings::{AppFlags, AppSettings};
pub use arg::Arg; pub use arg::Arg;
pub use arg_group::ArgGroup; pub use arg_group::ArgGroup;
@ -54,7 +55,6 @@ pub use command::App;
#[cfg(feature = "regex")] #[cfg(feature = "regex")]
pub use self::regex::RegexRef; pub use self::regex::RegexRef;
pub(crate) use action::ArgAction;
pub(crate) use arg::display_arg_val; pub(crate) use arg::display_arg_val;
pub(crate) use arg_predicate::ArgPredicate; pub(crate) use arg_predicate::ArgPredicate;
pub(crate) use value_parser::ValueParserInner; pub(crate) use value_parser::ValueParserInner;

View file

@ -347,7 +347,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
// get the option so we can check the settings // get the option so we can check the settings
let arg_values = matcher.pending_values_mut(id, None); let arg_values = matcher.pending_values_mut(id, None);
let arg = &self.cmd[id]; let arg = &self.cmd[id];
let parse_result = self.push_arg_values( let parse_result = self.split_arg_values(
arg, arg,
arg_os.to_value_os(), arg_os.to_value_os(),
trailing_values, trailing_values,
@ -389,7 +389,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} }
let arg_values = matcher.pending_values_mut(&arg.id, Some(Identifier::Index)); let arg_values = matcher.pending_values_mut(&arg.id, Some(Identifier::Index));
let _parse_result = let _parse_result =
self.push_arg_values(arg, arg_os.to_value_os(), trailing_values, arg_values); self.split_arg_values(arg, arg_os.to_value_os(), trailing_values, arg_values);
if let Some(_parse_result) = _parse_result { if let Some(_parse_result) = _parse_result {
if _parse_result != ParseResult::ValuesDone { if _parse_result != ParseResult::ValuesDone {
debug!( debug!(
@ -963,7 +963,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
debug!("Parser::parse_opt_value: has default_missing_vals"); debug!("Parser::parse_opt_value: has default_missing_vals");
for v in arg.default_missing_vals.iter() { for v in arg.default_missing_vals.iter() {
let trailing_values = false; // CLI should not be affecting default_missing_values let trailing_values = false; // CLI should not be affecting default_missing_values
let _parse_result = self.push_arg_values( let _parse_result = self.split_arg_values(
arg, arg,
&RawOsStr::new(v), &RawOsStr::new(v),
trailing_values, trailing_values,
@ -997,7 +997,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} }
} else if let Some(v) = attached_value { } else if let Some(v) = attached_value {
let mut arg_values = Vec::new(); let mut arg_values = Vec::new();
let parse_result = self.push_arg_values(arg, v, trailing_values, &mut arg_values); let parse_result = self.split_arg_values(arg, v, trailing_values, &mut arg_values);
let react_result = self.react( let react_result = self.react(
Some(ident), Some(ident),
ValueSource::CommandLine, ValueSource::CommandLine,
@ -1026,16 +1026,16 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} }
} }
fn push_arg_values( fn split_arg_values(
&self, &self,
arg: &Arg<'help>, arg: &Arg<'help>,
val: &RawOsStr, val: &RawOsStr,
trailing_values: bool, trailing_values: bool,
output: &mut Vec<OsString>, output: &mut Vec<OsString>,
) -> Option<ParseResult> { ) -> Option<ParseResult> {
debug!("Parser::push_arg_values; arg={}, val={:?}", arg.name, val); debug!("Parser::split_arg_values; arg={}, val={:?}", arg.name, val);
debug!( debug!(
"Parser::push_arg_values; trailing_values={:?}, DontDelimTrailingVals={:?}", "Parser::split_arg_values; trailing_values={:?}, DontDelimTrailingVals={:?}",
trailing_values, trailing_values,
self.cmd.is_dont_delimit_trailing_values_set() self.cmd.is_dont_delimit_trailing_values_set()
); );
@ -1070,13 +1070,13 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} }
} }
fn store_arg_values( fn push_arg_values(
&self, &self,
arg: &Arg<'help>, arg: &Arg<'help>,
raw_vals: Vec<OsString>, raw_vals: Vec<OsString>,
matcher: &mut ArgMatcher, matcher: &mut ArgMatcher,
) -> ClapResult<()> { ) -> ClapResult<()> {
debug!("Parser::store_arg_values: {:?}", raw_vals); debug!("Parser::push_arg_values: {:?}", raw_vals);
for raw_val in raw_vals { for raw_val in raw_vals {
// update the current index because each value is a distinct index to clap // update the current index because each value is a distinct index to clap
@ -1154,7 +1154,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} else { } else {
self.start_custom_arg(matcher, arg, source); self.start_custom_arg(matcher, arg, source);
} }
self.store_arg_values(arg, raw_vals, matcher)?; self.push_arg_values(arg, raw_vals, matcher)?;
if ident == Some(Identifier::Index) && arg.is_multiple_values_set() { if ident == Some(Identifier::Index) && arg.is_multiple_values_set() {
// HACK: Maintain existing occurrence behavior // HACK: Maintain existing occurrence behavior
let matched = matcher.get_mut(&arg.id).unwrap(); let matched = matcher.get_mut(&arg.id).unwrap();
@ -1167,7 +1167,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} }
Ok(ParseResult::ValuesDone) Ok(ParseResult::ValuesDone)
} }
ArgAction::Flag => { ArgAction::IncOccurrence => {
debug_assert_eq!(raw_vals, Vec::<OsString>::new()); debug_assert_eq!(raw_vals, Vec::<OsString>::new());
if source == ValueSource::CommandLine { if source == ValueSource::CommandLine {
if matches!(ident, Some(Identifier::Short) | Some(Identifier::Long)) { if matches!(ident, Some(Identifier::Short) | Some(Identifier::Long)) {
@ -1254,7 +1254,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
); );
let mut arg_values = Vec::new(); let mut arg_values = Vec::new();
let _parse_result = let _parse_result =
self.push_arg_values(arg, &val, trailing_values, &mut arg_values); self.split_arg_values(arg, &val, trailing_values, &mut arg_values);
let _ = self.react(None, ValueSource::EnvVariable, arg, arg_values, matcher)?; let _ = self.react(None, ValueSource::EnvVariable, arg, arg_values, matcher)?;
if let Some(_parse_result) = _parse_result { if let Some(_parse_result) = _parse_result {
if _parse_result != ParseResult::ValuesDone { if _parse_result != ParseResult::ValuesDone {
@ -1264,7 +1264,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} else { } else {
match arg.get_action() { match arg.get_action() {
ArgAction::StoreValue => unreachable!("{:?} is not a flag", arg.get_id()), ArgAction::StoreValue => unreachable!("{:?} is not a flag", arg.get_id()),
ArgAction::Flag => { ArgAction::IncOccurrence => {
debug!("Parser::add_env: Found a flag with value `{:?}`", val); debug!("Parser::add_env: Found a flag with value `{:?}`", val);
let predicate = str_to_bool(val.to_str_lossy()); let predicate = str_to_bool(val.to_str_lossy());
debug!("Parser::add_env: Found boolean literal `{:?}`", predicate); debug!("Parser::add_env: Found boolean literal `{:?}`", predicate);
@ -1324,7 +1324,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
// The flag occurred, we just want to add the val groups // The flag occurred, we just want to add the val groups
let mut arg_values = Vec::new(); let mut arg_values = Vec::new();
for v in arg.default_missing_vals.iter() { for v in arg.default_missing_vals.iter() {
let _parse_result = self.push_arg_values( let _parse_result = self.split_arg_values(
arg, arg,
&RawOsStr::new(v), &RawOsStr::new(v),
trailing_values, trailing_values,
@ -1337,7 +1337,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
} }
} }
self.start_custom_arg(matcher, arg, ValueSource::CommandLine); self.start_custom_arg(matcher, arg, ValueSource::CommandLine);
self.store_arg_values(arg, arg_values, matcher)?; self.push_arg_values(arg, arg_values, matcher)?;
} }
None => { None => {
debug!("Parser::add_default_value:iter:{}: wasn't used", arg.name); debug!("Parser::add_default_value:iter:{}: wasn't used", arg.name);
@ -1377,7 +1377,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
if add { if add {
if let Some(default) = default { if let Some(default) = default {
let mut arg_values = Vec::new(); let mut arg_values = Vec::new();
let _parse_result = self.push_arg_values( let _parse_result = self.split_arg_values(
arg, arg,
&RawOsStr::new(default), &RawOsStr::new(default),
trailing_values, trailing_values,
@ -1416,7 +1416,7 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
debug!("Parser::add_default_value:iter:{}: wasn't used", arg.name); debug!("Parser::add_default_value:iter:{}: wasn't used", arg.name);
let mut arg_values = Vec::new(); let mut arg_values = Vec::new();
for v in arg.default_vals.iter() { for v in arg.default_vals.iter() {
let _parse_result = self.push_arg_values( let _parse_result = self.split_arg_values(
arg, arg,
&RawOsStr::new(v), &RawOsStr::new(v),
trailing_values, trailing_values,