Merge pull request #2655 from ldm0/coverage

Parser refactor
This commit is contained in:
Pavan Kumar Sunkara 2021-08-08 20:56:09 +01:00 committed by GitHub
commit eabe828092
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 590 additions and 348 deletions

View file

@ -146,14 +146,10 @@ impl_settings! { AppSettings, AppFlags,
=> Flags::DISABLE_VERSION_FOR_SC,
WaitOnError("waitonerror")
=> Flags::WAIT_ON_ERROR,
TrailingValues("trailingvalues")
=> Flags::TRAILING_VALUES,
Built("built")
=> Flags::BUILT,
BinNameBuilt("binnamebuilt")
=> Flags::BIN_NAME_BUILT,
ValidArgFound("validargfound")
=> Flags::VALID_ARG_FOUND,
InferSubcommands("infersubcommands")
=> Flags::INFER_SUBCOMMANDS,
AllArgsOverrideSelf("allargsoverrideself")
@ -1099,10 +1095,6 @@ pub enum AppSettings {
/// @TODO-v3: @docs write them...maybe rename
NoAutoVersion,
#[doc(hidden)]
/// If the user already passed '--'. Meaning only positional args follow.
TrailingValues,
#[doc(hidden)]
/// If the app is already built, used for caching.
Built,
@ -1110,11 +1102,6 @@ pub enum AppSettings {
#[doc(hidden)]
/// If the app's bin name is already built, used for caching.
BinNameBuilt,
#[doc(hidden)]
/// This is paired with `ArgsNegateSubcommands`. Used to determine if we
/// already met any valid arg(then we shouldn't expect subcommands after it).
ValidArgFound,
}
#[cfg(test)]
@ -1258,19 +1245,11 @@ mod test {
"waitonerror".parse::<AppSettings>().unwrap(),
AppSettings::WaitOnError
);
assert_eq!(
"validargfound".parse::<AppSettings>().unwrap(),
AppSettings::ValidArgFound
);
assert_eq!("built".parse::<AppSettings>().unwrap(), AppSettings::Built);
assert_eq!(
"binnamebuilt".parse::<AppSettings>().unwrap(),
AppSettings::BinNameBuilt
);
assert_eq!(
"trailingvalues".parse::<AppSettings>().unwrap(),
AppSettings::TrailingValues
);
assert_eq!(
"infersubcommands".parse::<AppSettings>().unwrap(),
AppSettings::InferSubcommands

View file

@ -564,9 +564,8 @@ impl Error {
}
}
pub(crate) fn no_equals(arg: &Arg, usage: String, color: ColorChoice) -> Self {
pub(crate) fn no_equals(arg: String, usage: String, color: ColorChoice) -> Self {
let mut c = Colorizer::new(true, color);
let arg = arg.to_string();
start_error(&mut c, "Equal sign is needed when assigning values to '");
c.warning(&arg);
@ -798,7 +797,7 @@ impl Error {
pub(crate) fn too_many_values(
val: String,
arg: &Arg,
arg: String,
usage: String,
color: ColorChoice,
) -> Self {
@ -807,7 +806,7 @@ impl Error {
start_error(&mut c, "The value '");
c.warning(val.clone());
c.none("' was provided to '");
c.warning(arg.to_string());
c.warning(&arg);
c.none("' but it wasn't expecting any more values");
put_usage(&mut c, usage);
try_help(&mut c);
@ -815,7 +814,7 @@ impl Error {
Error {
message: c,
kind: ErrorKind::TooManyValues,
info: vec![arg.to_string(), val],
info: vec![arg, val],
source: None,
}
}

View file

@ -9,7 +9,7 @@ mod validator;
pub(crate) use self::{
arg_matcher::ArgMatcher,
matches::{MatchedArg, SubCommand, ValueType},
parser::{Input, ParseResult, Parser},
parser::{Input, ParseState, Parser},
validator::Validator,
};

View file

@ -20,17 +20,43 @@ use crate::{
INTERNAL_ERROR_MSG, INVALID_UTF8,
};
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum ParseResult {
Flag,
FlagSubCommand(String),
FlagSubCommandShort(String),
pub(crate) enum ParseState {
ValuesDone,
Opt(Id),
Pos(Id),
MaybeHyphenValue,
NotFound,
AttachedValueNotConsumed,
}
/// Recoverable Parsing results.
#[derive(Debug, PartialEq, Clone)]
enum ParseResult {
FlagSubCommand(String),
Opt(Id),
ValuesDone,
/// Value attached to the short flag is not consumed(e.g. 'u' for `-cu` is
/// not consumed).
AttachedValueNotConsumed,
/// This long flag doesn't need a value but is provided one.
UnneededAttachedValue {
rest: String,
used: Vec<Id>,
arg: String,
},
/// This flag might be an hyphen Value.
MaybeHyphenValue,
/// Equals required but not provided.
EqualsNotProvided {
arg: String,
},
/// Failed to match a Arg.
NoMatchingArg {
arg: String,
},
/// No argument found e.g. parser is given `-` when parsing a flag.
NoArg,
/// This is a Help flag.
HelpFlag,
/// This is a version flag.
VersionFlag,
}
#[derive(Debug)]
@ -319,8 +345,14 @@ impl<'help, 'app> Parser<'help, 'app> {
let mut subcmd_name: Option<String> = None;
let mut keep_state = false;
let mut needs_val_of = ParseResult::NotFound;
let mut parse_state = ParseState::ValuesDone;
let mut pos_counter = 1;
// Already met any valid arg(then we shouldn't expect subcommands after it).
let mut valid_arg_found = false;
// If the user already passed '--'. Meaning only positional args follow.
let mut trailing_values = false;
// Count of positional args
let positional_count = self.app.args.keys().filter(|x| x.is_position()).count();
// If any arg sets .last(true)
@ -349,14 +381,83 @@ impl<'help, 'app> Parser<'help, 'app> {
arg_os.as_raw_bytes()
);
// Has the user already passed '--'? Meaning only positional args follow
if !self.is_set(AS::TrailingValues) {
let can_be_subcommand = self.is_set(AS::SubcommandPrecedenceOverArg)
|| !matches!(needs_val_of, ParseResult::Opt(_) | ParseResult::Pos(_));
// Correct pos_counter.
pos_counter = {
let is_second_to_last = pos_counter + 1 == positional_count;
if can_be_subcommand {
// The last positional argument, or second to last positional
// argument may be set to .multiple_values(true)
let low_index_mults = is_second_to_last
&& self.app.get_positionals().any(|a| {
a.is_set(ArgSettings::MultipleValues)
&& (positional_count != a.index.unwrap_or(0))
})
&& self
.app
.get_positionals()
.last()
.map_or(false, |p_name| !p_name.is_set(ArgSettings::Last));
let missing_pos = self.is_set(AS::AllowMissingPositional)
&& is_second_to_last
&& !trailing_values;
debug!(
"Parser::get_matches_with: Positional counter...{}",
pos_counter
);
debug!(
"Parser::get_matches_with: Low index multiples...{:?}",
low_index_mults
);
if low_index_mults || missing_pos {
let skip_current = if let Some(n) = remaining_args.get(0) {
if let Some(p) = self
.app
.get_positionals()
.find(|p| p.index == Some(pos_counter))
{
// If next value looks like a new_arg or it's a
// subcommand, skip positional argument under current
// pos_counter(which means current value cannot be a
// positional argument with a value next to it), assume
// current value matches the next arg.
let n = ArgStr::new(n);
self.is_new_arg(&n, p)
|| self.possible_subcommand(&n, valid_arg_found).is_some()
} else {
true
}
} else {
true
};
if skip_current {
debug!("Parser::get_matches_with: Bumping the positional counter...");
pos_counter + 1
} else {
pos_counter
}
} else if trailing_values
&& (self.is_set(AS::AllowMissingPositional) || contains_last)
{
// Came to -- and one positional has .last(true) set, so we go immediately
// to the last (highest index) positional
debug!("Parser::get_matches_with: .last(true) and --, setting last pos");
positional_count
} else {
pos_counter
}
};
// Has the user already passed '--'? Meaning only positional args follow
if !trailing_values {
if self.is_set(AS::SubcommandPrecedenceOverArg)
|| !matches!(parse_state, ParseState::Opt(_) | ParseState::Pos(_))
{
// Does the arg match a subcommand name, or any of its aliases (if defined)
let sc_name = self.possible_subcommand(&arg_os);
let sc_name = self.possible_subcommand(&arg_os, valid_arg_found);
debug!("Parser::get_matches_with: sc={:?}", sc_name);
if let Some(sc_name) = sc_name {
if sc_name == "help" && !self.is_set(AS::NoAutoHelp) {
@ -367,50 +468,107 @@ impl<'help, 'app> Parser<'help, 'app> {
}
}
// Is this a new argument, or a value for previous option?
if self.is_new_arg(&arg_os, &needs_val_of) {
if arg_os == "--" {
debug!("Parser::get_matches_with: setting TrailingVals=true");
self.app.set(AS::TrailingValues);
continue;
} else if arg_os.starts_with("--") {
needs_val_of = self.parse_long_arg(matcher, &arg_os, remaining_args)?;
if arg_os.starts_with("--") {
let parse_result = self.parse_long_arg(
matcher,
&arg_os,
&parse_state,
&mut valid_arg_found,
trailing_values,
);
debug!(
"Parser::get_matches_with: After parse_long_arg {:?}",
needs_val_of
parse_result
);
match needs_val_of {
ParseResult::Flag | ParseResult::Opt(..) | ParseResult::ValuesDone => {
match parse_result {
ParseResult::NoArg => {
debug!("Parser::get_matches_with: setting TrailingVals=true");
trailing_values = true;
continue;
}
ParseResult::FlagSubCommand(ref name) => {
debug!("Parser::get_matches_with: FlagSubCommand found in long arg {:?}", name);
subcmd_name = Some(name.to_owned());
ParseResult::ValuesDone => {
parse_state = ParseState::ValuesDone;
continue;
}
ParseResult::Opt(id) => {
parse_state = ParseState::Opt(id);
continue;
}
ParseResult::FlagSubCommand(name) => {
debug!(
"Parser::get_matches_with: FlagSubCommand found in long arg {:?}",
&name
);
subcmd_name = Some(name);
break;
}
_ => (),
ParseResult::EqualsNotProvided { arg } => {
return Err(ClapError::no_equals(
arg,
Usage::new(self).create_usage_with_title(&[]),
self.app.color(),
));
}
} else if arg_os.starts_with("-")
&& arg_os.len() != 1
&& !(self.is_set(AS::AllowNegativeNumbers)
&& arg_os.to_string_lossy().parse::<f64>().is_ok())
{
ParseResult::NoMatchingArg { arg } => {
let remaining_args: Vec<_> = remaining_args
.iter()
.map(|x| x.to_str().expect(INVALID_UTF8))
.collect();
return Err(self.did_you_mean_error(&arg, matcher, &remaining_args));
}
ParseResult::UnneededAttachedValue { rest, used, arg } => {
return Err(ClapError::too_many_values(
rest,
arg,
Usage::new(self).create_usage_no_title(&used),
self.app.color(),
))
}
ParseResult::HelpFlag => {
return Err(self.help_err(true));
}
ParseResult::VersionFlag => {
return Err(self.version_err(true));
}
ParseResult::MaybeHyphenValue => {
// Maybe a hyphen value, do nothing.
}
ParseResult::AttachedValueNotConsumed => {
unreachable!()
}
}
} else if arg_os.starts_with("-") {
// Arg looks like a short flag, and not a possible number
// Try to parse short args like normal, if AllowLeadingHyphen or
// AllowNegativeNumbers is set, parse_short_arg will *not* throw
// an error, and instead return Ok(None)
needs_val_of = self.parse_short_arg(matcher, &arg_os)?;
let parse_result = self.parse_short_arg(
matcher,
&arg_os,
&parse_state,
pos_counter,
&mut valid_arg_found,
trailing_values,
);
// If it's None, we then check if one of those two AppSettings was set
debug!(
"Parser::get_matches_with: After parse_short_arg {:?}",
needs_val_of
parse_result
);
match needs_val_of {
ParseResult::Opt(..) | ParseResult::Flag | ParseResult::ValuesDone => {
match parse_result {
ParseResult::NoArg => {
// Is a single dash `-`, try positional.
}
ParseResult::ValuesDone => {
parse_state = ParseState::ValuesDone;
continue;
}
ParseResult::FlagSubCommandShort(ref name) => {
ParseResult::Opt(id) => {
parse_state = ParseState::Opt(id);
continue;
}
ParseResult::FlagSubCommand(name) => {
// If there are more short flags to be processed, we should keep the state, and later
// revisit the current group of short flags skipping the subcommand.
keep_state = self
@ -430,95 +588,62 @@ impl<'help, 'app> Parser<'help, 'app> {
self.flag_subcmd_skip
);
subcmd_name = Some(name.to_owned());
subcmd_name = Some(name);
break;
}
_ => (),
ParseResult::EqualsNotProvided { arg } => {
return Err(ClapError::no_equals(
arg,
Usage::new(self).create_usage_with_title(&[]),
self.app.color(),
))
}
ParseResult::NoMatchingArg { arg } => {
return Err(ClapError::unknown_argument(
arg,
None,
Usage::new(self).create_usage_with_title(&[]),
self.app.color(),
));
}
ParseResult::HelpFlag => {
return Err(self.help_err(false));
}
ParseResult::VersionFlag => {
return Err(self.version_err(false));
}
ParseResult::MaybeHyphenValue => {
// Maybe a hyphen value, do nothing.
}
ParseResult::UnneededAttachedValue { .. }
| ParseResult::AttachedValueNotConsumed => unreachable!(),
}
}
} else if let ParseResult::Opt(id) = needs_val_of {
// Check to see if parsing a value from a previous arg
if let ParseState::Opt(id) = &parse_state {
// Assume this is a value of a previous arg.
// get the option so we can check the settings
needs_val_of = self.add_val_to_arg(
&self.app[&id],
let parse_result = self.add_val_to_arg(
&self.app[id],
arg_os,
matcher,
ValueType::CommandLine,
true,
trailing_values,
);
parse_state = match parse_result {
ParseResult::Opt(id) => ParseState::Opt(id),
ParseResult::ValuesDone => ParseState::ValuesDone,
_ => unreachable!(),
};
// get the next value from the iterator
continue;
}
}
let is_second_to_last = pos_counter + 1 == positional_count;
// The last positional argument, or second to last positional
// argument may be set to .multiple_values(true)
let low_index_mults = is_second_to_last
&& self.app.get_positionals().any(|a| {
a.is_set(ArgSettings::MultipleValues)
&& (positional_count != a.index.unwrap_or(0))
})
&& self
.app
.get_positionals()
.last()
.map_or(false, |p_name| !p_name.is_set(ArgSettings::Last));
let missing_pos = self.is_set(AS::AllowMissingPositional)
&& is_second_to_last
&& !self.is_set(AS::TrailingValues);
debug!(
"Parser::get_matches_with: Positional counter...{}",
pos_counter
);
debug!(
"Parser::get_matches_with: Low index multiples...{:?}",
low_index_mults
);
pos_counter = if low_index_mults || missing_pos {
let bump = if let Some(n) = remaining_args.get(0) {
needs_val_of = if let Some(p) = self
.app
.get_positionals()
.find(|p| p.index == Some(pos_counter))
{
ParseResult::Pos(p.id.clone())
} else {
ParseResult::ValuesDone
};
// If the arg doesn't looks like a new_arg and it's not a
// subcommand, don't bump the position counter.
let n = ArgStr::new(n);
self.is_new_arg(&n, &needs_val_of) || self.possible_subcommand(&n).is_some()
} else {
true
};
if bump {
debug!("Parser::get_matches_with: Bumping the positional counter...");
pos_counter + 1
} else {
pos_counter
}
} else if self.is_set(AS::TrailingValues)
&& (self.is_set(AS::AllowMissingPositional) || contains_last)
{
// Came to -- and one positional has .last(true) set, so we go immediately
// to the last (highest index) positional
debug!("Parser::get_matches_with: .last(true) and --, setting last pos");
positional_count
} else {
pos_counter
};
if let Some(p) = self.app.args.get(&pos_counter) {
if p.is_set(ArgSettings::Last) && !self.is_set(AS::TrailingValues) {
if p.is_set(ArgSettings::Last) && !trailing_values {
return Err(ClapError::unknown_argument(
arg_os.to_string_lossy().to_string(),
None,
@ -528,7 +653,7 @@ impl<'help, 'app> Parser<'help, 'app> {
}
if self.is_set(AS::TrailingVarArg) && pos_counter == positional_count {
self.app.settings.set(AS::TrailingValues);
trailing_values = true;
}
self.seen.push(p.id.clone());
@ -536,7 +661,14 @@ impl<'help, 'app> Parser<'help, 'app> {
// doesn't have any value. This behaviour is right because
// positional arguments are always present continiously.
let append = self.arg_have_val(matcher, p);
self.add_val_to_arg(p, arg_os, matcher, ValueType::CommandLine, append);
self.add_val_to_arg(
p,
arg_os,
matcher,
ValueType::CommandLine,
append,
trailing_values,
);
// Increase occurrence no matter if we are appending, occurrences
// of positional argument equals to number of values rather than
@ -546,8 +678,11 @@ impl<'help, 'app> Parser<'help, 'app> {
// Only increment the positional counter if it doesn't allow multiples
if !p.settings.is_set(ArgSettings::MultipleValues) {
pos_counter += 1;
parse_state = ParseState::ValuesDone;
} else {
parse_state = ParseState::Pos(p.id.clone());
}
self.app.settings.set(AS::ValidArgFound);
valid_arg_found = true;
} else if self.is_set(AS::AllowExternalSubcommands) {
// Get external subcommand name
let sc_name = match arg_os.to_str() {
@ -589,10 +724,15 @@ impl<'help, 'app> Parser<'help, 'app> {
self.remove_overrides(matcher);
return Validator::new(self).validate(needs_val_of, subcmd_name.is_some(), matcher);
return Validator::new(self).validate(
parse_state,
subcmd_name.is_some(),
matcher,
trailing_values,
);
} else {
// Start error processing
return Err(self.match_arg_error(&arg_os));
return Err(self.match_arg_error(&arg_os, valid_arg_found, trailing_values));
}
}
@ -624,14 +764,19 @@ impl<'help, 'app> Parser<'help, 'app> {
self.remove_overrides(matcher);
Validator::new(self).validate(needs_val_of, subcmd_name.is_some(), matcher)
Validator::new(self).validate(parse_state, subcmd_name.is_some(), matcher, trailing_values)
}
fn match_arg_error(&self, arg_os: &ArgStr) -> ClapError {
fn match_arg_error(
&self,
arg_os: &ArgStr,
valid_arg_found: bool,
trailing_values: bool,
) -> ClapError {
// If argument follows a `--`
if self.is_set(AS::TrailingValues) {
if trailing_values {
// If the arg matches a subcommand name, or any of its aliases (if defined)
if self.possible_subcommand(arg_os).is_some() {
if self.possible_subcommand(arg_os, valid_arg_found).is_some() {
return ClapError::unnecessary_double_dash(
arg_os.to_string_lossy().to_string(),
Usage::new(self).create_usage_with_title(&[]),
@ -691,10 +836,10 @@ impl<'help, 'app> Parser<'help, 'app> {
}
// Checks if the arg matches a subcommand name, or any of its aliases (if defined)
fn possible_subcommand(&self, arg_os: &ArgStr) -> Option<&str> {
fn possible_subcommand(&self, arg_os: &ArgStr, valid_arg_found: bool) -> Option<&str> {
debug!("Parser::possible_subcommand: arg={:?}", arg_os);
if !(self.is_set(AS::ArgsNegateSubcommands) && self.is_set(AS::ValidArgFound)) {
if !(self.is_set(AS::ArgsNegateSubcommands) && valid_arg_found) {
if self.is_set(AS::InferSubcommands) {
// For subcommand `test`, we accepts it's prefix: `t`, `te`,
// `tes` and `test`.
@ -824,34 +969,32 @@ impl<'help, 'app> Parser<'help, 'app> {
Err(parser.help_err(self.app.is_set(AS::UseLongFormatForHelpSubcommand)))
}
fn is_new_arg(&self, arg_os: &ArgStr, last_result: &ParseResult) -> bool {
debug!("Parser::is_new_arg: {:?}:{:?}", arg_os, last_result);
fn is_new_arg(&self, next: &ArgStr, current_positional: &Arg) -> bool {
debug!(
"Parser::is_new_arg: {:?}:{:?}",
next, current_positional.name
);
let want_value = match last_result {
ParseResult::Opt(name) | ParseResult::Pos(name) => {
self.is_set(AS::AllowLeadingHyphen)
|| self.app[name].is_set(ArgSettings::AllowHyphenValues)
if self.is_set(AS::AllowLeadingHyphen)
|| self.app[&current_positional.id].is_set(ArgSettings::AllowHyphenValues)
|| (self.is_set(AS::AllowNegativeNumbers)
&& arg_os.to_string_lossy().parse::<f64>().is_ok())
}
ParseResult::ValuesDone => return true,
_ => false,
};
debug!("Parser::is_new_arg: want_value={:?}", want_value);
// Is this a new argument, or values from a previous option?
if want_value {
&& next.to_string_lossy().parse::<f64>().is_ok())
{
// If allow hyphen, this isn't a new arg.
debug!("Parser::is_new_arg: Allow hyphen");
false
} else if arg_os.starts_with("--") {
} else if next.starts_with("--") {
// If this is a long flag, this is a new arg.
debug!("Parser::is_new_arg: -- found");
true
} else if arg_os.starts_with("-") {
} else if next.starts_with("-") {
debug!("Parser::is_new_arg: - found");
// a singe '-' by itself is a value and typically means "stdin" on unix systems
arg_os.len() != 1
// If this is a short flag, this is a new arg. But a singe '-' by
// itself is a value and typically means "stdin" on unix systems.
next.len() != 1
} else {
debug!("Parser::is_new_arg: value");
// Nothing special, this is a value.
false
}
}
@ -950,7 +1093,7 @@ impl<'help, 'app> Parser<'help, 'app> {
// Retrieves the names of all args the user has supplied thus far, except required ones
// because those will be listed in self.required
fn check_for_help_and_version_str(&self, arg: &ArgStr) -> ClapResult<()> {
fn check_for_help_and_version_str(&self, arg: &ArgStr) -> Option<ParseResult> {
debug!("Parser::check_for_help_and_version_str");
debug!(
"Parser::check_for_help_and_version_str: Checking if --{:?} is help or version...",
@ -961,7 +1104,7 @@ impl<'help, 'app> Parser<'help, 'app> {
if let Some(h) = help.long {
if arg == h && !self.is_set(AS::NoAutoHelp) {
debug!("Help");
return Err(self.help_err(true));
return Some(ParseResult::HelpFlag);
}
}
}
@ -970,16 +1113,16 @@ impl<'help, 'app> Parser<'help, 'app> {
if let Some(v) = version.long {
if arg == v && !self.is_set(AS::NoAutoVersion) {
debug!("Version");
return Err(self.version_err(true));
return Some(ParseResult::VersionFlag);
}
}
}
debug!("Neither");
Ok(())
None
}
fn check_for_help_and_version_char(&self, arg: char) -> ClapResult<()> {
fn check_for_help_and_version_char(&self, arg: char) -> Option<ParseResult> {
debug!("Parser::check_for_help_and_version_char");
debug!(
"Parser::check_for_help_and_version_char: Checking if -{} is help or version...",
@ -990,7 +1133,7 @@ impl<'help, 'app> Parser<'help, 'app> {
if let Some(h) = help.short {
if arg == h && !self.is_set(AS::NoAutoHelp) {
debug!("Help");
return Err(self.help_err(false));
return Some(ParseResult::HelpFlag);
}
}
}
@ -999,13 +1142,13 @@ impl<'help, 'app> Parser<'help, 'app> {
if let Some(v) = version.short {
if arg == v && !self.is_set(AS::NoAutoVersion) {
debug!("Version");
return Err(self.version_err(false));
return Some(ParseResult::VersionFlag);
}
}
}
debug!("Neither");
Ok(())
None
}
fn use_long_help(&self) -> bool {
@ -1031,17 +1174,28 @@ impl<'help, 'app> Parser<'help, 'app> {
&mut self,
matcher: &mut ArgMatcher,
full_arg: &ArgStr,
remaining_args: &[OsString],
) -> ClapResult<ParseResult> {
parse_state: &ParseState,
valid_arg_found: &mut bool,
trailing_values: bool,
) -> ParseResult {
// maybe here lifetime should be 'a
debug!("Parser::parse_long_arg");
if matches!(parse_state, ParseState::Opt(opt) | ParseState::Pos(opt) if
self.app[opt].is_set(ArgSettings::AllowHyphenValues))
{
return ParseResult::MaybeHyphenValue;
}
// Update the current index
self.cur_idx.set(self.cur_idx.get() + 1);
debug!("Parser::parse_long_arg: cur_idx:={}", self.cur_idx.get());
debug!("Parser::parse_long_arg: Does it contain '='...");
let long_arg = full_arg.trim_start_n_matches(2, b'-');
if long_arg.is_empty() {
return ParseResult::NoArg;
}
let (arg, val) = if full_arg.contains_byte(b'=') {
let (p0, p1) = long_arg.split_at_byte(b'=');
debug!("Yes '{:?}'", p1);
@ -1071,17 +1225,15 @@ impl<'help, 'app> Parser<'help, 'app> {
};
if let Some(opt) = opt {
self.app.settings.set(AS::ValidArgFound);
*valid_arg_found = true;
self.seen.push(opt.id.clone());
if opt.is_set(ArgSettings::TakesValue) {
debug!(
"Parser::parse_long_arg: Found an opt with value '{:?}'",
&val
);
return self.parse_opt(&val, opt, matcher);
}
debug!("Parser::parse_long_arg: Found a flag");
if let Some(rest) = val {
self.parse_opt(&val, opt, matcher, trailing_values)
} else if let Some(rest) = val {
debug!(
"Parser::parse_long_arg: Got invalid literal `{:?}`",
rest.to_os_string()
@ -1096,33 +1248,25 @@ impl<'help, 'app> Parser<'help, 'app> {
.cloned()
.collect();
return Err(ClapError::too_many_values(
rest.to_string_lossy().into(),
opt,
Usage::new(self).create_usage_no_title(&used),
self.app.color(),
));
ParseResult::UnneededAttachedValue {
rest: rest.to_string_lossy().to_string(),
used,
arg: opt.to_string(),
}
self.check_for_help_and_version_str(&arg)?;
debug!("Parser::parse_long_arg: Presence validated");
return Ok(self.parse_flag(opt, matcher));
}
if let Some(sc_name) = self.possible_long_flag_subcommand(&arg) {
Ok(ParseResult::FlagSubCommand(sc_name.to_string()))
} else if self.is_set(AS::AllowLeadingHyphen) {
Ok(ParseResult::MaybeHyphenValue)
} else if let Some(parse_result) = self.check_for_help_and_version_str(&arg) {
parse_result
} else {
debug!("Parser::parse_long_arg: Didn't match anything");
let remaining_args: Vec<_> = remaining_args
.iter()
.map(|x| x.to_str().expect(INVALID_UTF8))
.collect();
Err(self.did_you_mean_error(
arg.to_str().expect(INVALID_UTF8),
matcher,
&remaining_args,
))
debug!("Parser::parse_long_arg: Presence validated");
self.parse_flag(opt, matcher)
}
} else if let Some(sc_name) = self.possible_long_flag_subcommand(&arg) {
ParseResult::FlagSubCommand(sc_name.to_string())
} else if self.is_set(AS::AllowLeadingHyphen) {
ParseResult::MaybeHyphenValue
} else {
ParseResult::NoMatchingArg {
arg: arg.to_string_lossy().to_string(),
}
}
}
@ -1130,22 +1274,31 @@ impl<'help, 'app> Parser<'help, 'app> {
&mut self,
matcher: &mut ArgMatcher,
full_arg: &ArgStr,
) -> ClapResult<ParseResult> {
parse_state: &ParseState,
// change this to possible pos_arg when removing the usage of &mut Parser.
pos_counter: usize,
valid_arg_found: &mut bool,
trailing_values: bool,
) -> ParseResult {
debug!("Parser::parse_short_arg: full_arg={:?}", full_arg);
let arg_os = full_arg.trim_start_matches(b'-');
let arg = arg_os.to_string_lossy();
// If AllowLeadingHyphen is set, we want to ensure `-val` gets parsed as `-val` and not
// `-v` `-a` `-l` assuming `v` `a` and `l` are all, or mostly, valid shorts.
if self.is_set(AS::AllowLeadingHyphen) && arg.chars().any(|c| !self.app.contains_short(c)) {
debug!(
"Parser::parse_short_arg: LeadingHyphenAllowed yet -{} isn't valid",
arg
);
return Ok(ParseResult::MaybeHyphenValue);
if (self.is_set(AS::AllowNegativeNumbers) && arg.parse::<f64>().is_ok())
|| (self.is_set(AS::AllowLeadingHyphen)
&& arg.chars().any(|c| !self.app.contains_short(c)))
|| matches!(parse_state, ParseState::Opt(opt) | ParseState::Pos(opt)
if self.app[opt].is_set(ArgSettings::AllowHyphenValues))
|| self
.app
.args
.get(&pos_counter)
.map_or(false, |arg| arg.is_set(ArgSettings::AllowHyphenValues))
{
return ParseResult::MaybeHyphenValue;
}
let mut ret = ParseResult::NotFound;
let mut ret = ParseResult::NoArg;
let skip = self.flag_subcmd_skip;
self.flag_subcmd_skip = 0;
@ -1169,10 +1322,12 @@ impl<'help, 'app> Parser<'help, 'app> {
"Parser::parse_short_arg:iter:{}: Found valid opt or flag",
c
);
self.app.settings.set(AS::ValidArgFound);
*valid_arg_found = true;
self.seen.push(opt.id.clone());
if !opt.is_set(ArgSettings::TakesValue) {
self.check_for_help_and_version_char(c)?;
if let Some(parse_result) = self.check_for_help_and_version_char(c) {
return parse_result;
}
ret = self.parse_flag(opt, matcher);
continue;
}
@ -1204,11 +1359,13 @@ impl<'help, 'app> Parser<'help, 'app> {
//
// e.g. `-xvf`, when RequireEquals && x.min_vals == 0, we don't
// consume the `vf`, even if it's provided as value.
match self.parse_opt(&val, opt, matcher)? {
match self.parse_opt(&val, opt, matcher, trailing_values) {
ParseResult::AttachedValueNotConsumed => continue,
x => return Ok(x),
x => return x,
}
} else if let Some(sc_name) = self.app.find_short_subcmd(c) {
}
return if let Some(sc_name) = self.app.find_short_subcmd(c) {
debug!("Parser::parse_short_arg:iter:{}: subcommand={}", c, sc_name);
let name = sc_name.to_string();
let done_short_args = {
@ -1225,19 +1382,14 @@ impl<'help, 'app> Parser<'help, 'app> {
if done_short_args {
self.flag_subcmd_at = None;
}
return Ok(ParseResult::FlagSubCommandShort(name));
ParseResult::FlagSubCommand(name)
} else {
let arg = format!("-{}", c);
return Err(ClapError::unknown_argument(
arg,
None,
Usage::new(self).create_usage_with_title(&[]),
self.app.color(),
));
ParseResult::NoMatchingArg {
arg: format!("-{}", c),
}
};
}
Ok(ret)
ret
}
fn parse_opt(
@ -1245,7 +1397,8 @@ impl<'help, 'app> Parser<'help, 'app> {
attached_value: &Option<ArgStr>,
opt: &Arg<'help>,
matcher: &mut ArgMatcher,
) -> ClapResult<ParseResult> {
trailing_values: bool,
) -> ParseResult {
debug!(
"Parser::parse_opt; opt={}, val={:?}",
opt.name, attached_value
@ -1272,39 +1425,34 @@ impl<'help, 'app> Parser<'help, 'app> {
};
self.inc_occurrence_of_arg(matcher, opt);
if attached_value.is_some() {
return Ok(ParseResult::AttachedValueNotConsumed);
ParseResult::AttachedValueNotConsumed
} else {
return Ok(ParseResult::ValuesDone);
ParseResult::ValuesDone
}
} else {
debug!("Requires equals but not provided. Error.");
return Err(ClapError::no_equals(
opt,
Usage::new(self).create_usage_with_title(&[]),
self.app.color(),
));
ParseResult::EqualsNotProvided {
arg: opt.to_string(),
}
}
if let Some(fv) = attached_value {
} else if let Some(fv) = attached_value {
let v = fv.trim_start_n_matches(1, b'=');
if opt.is_set(ArgSettings::ForbidEmptyValues) && v.is_empty() {
debug!("Found Empty - Error");
return Err(ClapError::empty_value(
opt,
Usage::new(self).create_usage_with_title(&[]),
self.app.color(),
));
}
debug!("Found - {:?}, len: {}", v, v.len());
debug!(
"Parser::parse_opt: {:?} contains '='...{:?}",
fv,
fv.starts_with("=")
);
self.add_val_to_arg(opt, v, matcher, ValueType::CommandLine, false);
self.add_val_to_arg(
opt,
v,
matcher,
ValueType::CommandLine,
false,
trailing_values,
);
self.inc_occurrence_of_arg(matcher, opt);
Ok(ParseResult::ValuesDone)
ParseResult::ValuesDone
} else {
debug!("Parser::parse_opt: More arg vals required...");
self.inc_occurrence_of_arg(matcher, opt);
@ -1312,7 +1460,7 @@ impl<'help, 'app> Parser<'help, 'app> {
for group in self.app.groups_for_arg(&opt.id) {
matcher.new_val_group(&group);
}
Ok(ParseResult::Opt(opt.id.clone()))
ParseResult::Opt(opt.id.clone())
}
}
@ -1323,14 +1471,15 @@ impl<'help, 'app> Parser<'help, 'app> {
matcher: &mut ArgMatcher,
ty: ValueType,
append: bool,
trailing_values: bool,
) -> ParseResult {
debug!("Parser::add_val_to_arg; arg={}, val={:?}", arg.name, val);
debug!(
"Parser::add_val_to_arg; trailing_vals={:?}, DontDelimTrailingVals={:?}",
self.is_set(AS::TrailingValues),
"Parser::add_val_to_arg; trailing_values={:?}, DontDelimTrailingVals={:?}",
trailing_values,
self.is_set(AS::DontDelimitTrailingValues)
);
if !(self.is_set(AS::TrailingValues) && self.is_set(AS::DontDelimitTrailingValues)) {
if !(trailing_values && self.is_set(AS::DontDelimitTrailingValues)) {
if let Some(delim) = arg.val_delim {
let arg_split = val.split(delim);
let vals = if let Some(t) = arg.terminator {
@ -1434,7 +1583,7 @@ impl<'help, 'app> Parser<'help, 'app> {
matcher.add_index_to(&flag.id, self.cur_idx.get(), ValueType::CommandLine);
self.inc_occurrence_of_arg(matcher, flag);
ParseResult::Flag
ParseResult::ValuesDone
}
fn remove_overrides(&mut self, matcher: &mut ArgMatcher) {
@ -1501,21 +1650,27 @@ impl<'help, 'app> Parser<'help, 'app> {
}
}
pub(crate) fn add_defaults(&mut self, matcher: &mut ArgMatcher) {
pub(crate) fn add_defaults(&mut self, matcher: &mut ArgMatcher, trailing_values: bool) {
debug!("Parser::add_defaults");
for o in self.app.get_opts() {
debug!("Parser::add_defaults:iter:{}:", o.name);
self.add_value(o, matcher, ValueType::DefaultValue);
self.add_value(o, matcher, ValueType::DefaultValue, trailing_values);
}
for p in self.app.get_positionals() {
debug!("Parser::add_defaults:iter:{}:", p.name);
self.add_value(p, matcher, ValueType::DefaultValue);
self.add_value(p, matcher, ValueType::DefaultValue, trailing_values);
}
}
fn add_value(&self, arg: &Arg<'help>, matcher: &mut ArgMatcher, ty: ValueType) {
fn add_value(
&self,
arg: &Arg<'help>,
matcher: &mut ArgMatcher,
ty: ValueType,
trailing_values: bool,
) {
if !arg.default_vals_ifs.is_empty() {
debug!("Parser::add_value: has conditional defaults");
if matcher.get(&arg.id).is_none() {
@ -1532,7 +1687,14 @@ impl<'help, 'app> Parser<'help, 'app> {
if add {
if let Some(default) = default {
self.add_val_to_arg(arg, ArgStr::new(default), matcher, ty, false);
self.add_val_to_arg(
arg,
ArgStr::new(default),
matcher,
ty,
false,
trailing_values,
);
}
return;
}
@ -1605,7 +1767,11 @@ impl<'help, 'app> Parser<'help, 'app> {
}
}
pub(crate) fn add_env(&mut self, matcher: &mut ArgMatcher) -> ClapResult<()> {
pub(crate) fn add_env(
&mut self,
matcher: &mut ArgMatcher,
trailing_values: bool,
) -> ClapResult<()> {
if self.app.is_set(AS::DisableEnv) {
return Ok(());
}
@ -1616,9 +1782,24 @@ impl<'help, 'app> Parser<'help, 'app> {
if let Some((_, Some(ref val))) = a.env {
let val = ArgStr::new(val);
if a.is_set(ArgSettings::TakesValue) {
self.add_val_to_arg(a, val, matcher, ValueType::EnvVariable, false);
self.add_val_to_arg(
a,
val,
matcher,
ValueType::EnvVariable,
false,
trailing_values,
);
} else {
self.check_for_help_and_version_str(&val)?;
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));
}
_ => (),
}
matcher.add_index_to(&a.id, self.cur_idx.get(), ValueType::EnvVariable);
}
}

View file

@ -4,7 +4,7 @@ use crate::{
output::Usage,
parse::{
errors::{Error, ErrorKind, Result as ClapResult},
ArgMatcher, MatchedArg, ParseResult, Parser, ValueType,
ArgMatcher, MatchedArg, ParseState, Parser, ValueType,
},
util::{ChildGraph, Id},
INTERNAL_ERROR_MSG, INVALID_UTF8,
@ -25,15 +25,16 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
pub(crate) fn validate(
&mut self,
needs_val_of: ParseResult,
parse_state: ParseState,
is_subcmd: bool,
matcher: &mut ArgMatcher,
trailing_values: bool,
) -> ClapResult<()> {
debug!("Validator::validate");
let mut reqs_validated = false;
self.p.add_env(matcher)?;
self.p.add_defaults(matcher);
if let ParseResult::Opt(a) = needs_val_of {
self.p.add_env(matcher, trailing_values)?;
self.p.add_defaults(matcher, trailing_values);
if let ParseState::Opt(a) = parse_state {
debug!("Validator::validate: needs_val_of={:?}", a);
self.validate_required(matcher)?;
@ -506,7 +507,7 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
.to_str()
.expect(INVALID_UTF8)
.to_string(),
a,
a.to_string(),
Usage::new(self.p).create_usage_with_title(&[]),
self.p.app.color(),
));

View file

@ -7,6 +7,7 @@ use std::{
use os_str_bytes::{raw, OsStrBytes};
#[derive(PartialEq, Eq)]
pub(crate) struct ArgStr<'a>(Cow<'a, [u8]>);
impl<'a> ArgStr<'a> {
@ -27,58 +28,47 @@ impl<'a> ArgStr<'a> {
}
pub(crate) fn contains_byte(&self, byte: u8) -> bool {
assert!(byte.is_ascii());
debug_assert!(byte.is_ascii());
self.0.contains(&byte)
}
pub(crate) fn contains_char(&self, ch: char) -> bool {
let mut bytes = [0; 4];
let bytes = ch.encode_utf8(&mut bytes).as_bytes();
for i in 0..self.0.len().saturating_sub(bytes.len() - 1) {
if self.0[i..].starts_with(bytes) {
return true;
}
}
false
self.to_string_lossy().contains(|x| x == ch)
}
pub(crate) fn split_at_byte(&self, byte: u8) -> (ArgStr, ArgStr) {
assert!(byte.is_ascii());
debug_assert!(byte.is_ascii());
for (i, b) in self.0.iter().enumerate() {
if b == &byte {
return self.split_at_unchecked(i);
}
}
if let Some(i) = self.0.iter().position(|&x| x == byte) {
self.split_at_unchecked(i)
} else {
(self.to_borrowed(), Self(Cow::Borrowed(&[])))
}
}
pub(crate) fn trim_start_matches(&'a self, byte: u8) -> ArgStr {
assert!(byte.is_ascii());
debug_assert!(byte.is_ascii());
let mut found = false;
for (i, b) in self.0.iter().enumerate() {
if b != &byte {
return Self(Cow::Borrowed(&self.0[i..]));
if let Some(i) = self.0.iter().position(|x| x != &byte) {
Self(Cow::Borrowed(&self.0[i..]))
} else {
found = true;
Self(Cow::Borrowed(&[]))
}
}
if found {
return Self(Cow::Borrowed(&[]));
}
self.to_borrowed()
}
// Like `trim_start_matches`, but trims no more than `n` matches
#[inline]
pub(crate) fn trim_start_n_matches(&self, n: usize, ch: u8) -> ArgStr {
assert!(ch.is_ascii());
pub(crate) fn trim_start_n_matches(&'a self, n: usize, byte: u8) -> ArgStr {
debug_assert!(byte.is_ascii());
let i = self.0.iter().take(n).take_while(|c| **c == ch).count();
self.split_at_unchecked(i).1
if let Some(i) = self.0.iter().take(n).position(|x| x != &byte) {
Self(Cow::Borrowed(&self.0[i..]))
} else {
match self.0.get(n..) {
Some(x) => Self(Cow::Borrowed(x)),
None => Self(Cow::Borrowed(&[])),
}
}
}
pub(crate) fn split_at_unchecked(&'a self, i: usize) -> (ArgStr, ArgStr) {
@ -107,6 +97,7 @@ impl<'a> ArgStr<'a> {
self.0.len()
}
#[allow(dead_code)]
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
@ -187,3 +178,81 @@ impl<'a> Iterator for ArgSplit<'a> {
Some(ArgStr(Cow::Borrowed(&self.val[start..])))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
#[rustfmt::skip]
fn test_trim_start_matches() {
let raw = OsString::from("hello? world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_matches(b'-');
assert_eq!(trimmed, a);
let raw = OsString::from("------------hello? world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_matches(b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("hello? world")));
let raw = OsString::from("------------hel-lo? -world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_matches(b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("hel-lo? -world")));
let raw = OsString::from("hel-lo? -world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_matches(b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("hel-lo? -world")));
let raw = OsString::from("");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_matches(b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("")));
}
#[test]
#[rustfmt::skip]
fn test_trim_start_n_matches() {
let raw = OsString::from("hello? world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(2, b'-');
assert_eq!(trimmed, a);
let raw = OsString::from("------------hello? world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(2, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("----------hello? world")));
let raw = OsString::from("------------hello? world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(1000, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("hello? world")));
let raw = OsString::from("------------hel-lo? -world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(2, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("----------hel-lo? -world")));
let raw = OsString::from("-hel-lo? -world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(5, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("hel-lo? -world")));
let raw = OsString::from("hel-lo? -world");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(10, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("hel-lo? -world")));
let raw = OsString::from("");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(10, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("")));
let raw = OsString::from("");
let a = ArgStr::new(&raw);
let trimmed = a.trim_start_n_matches(0, b'-');
assert_eq!(trimmed, ArgStr::new(&OsString::from("")));
}
}

View file

@ -842,6 +842,19 @@ fn issue_1066_allow_leading_hyphen_and_unknown_args_option() {
assert_eq!(res.unwrap_err().kind, ErrorKind::UnknownArgument);
}
#[test]
fn issue_1437_allow_hyphen_values_for_positional_arg() {
let m = App::new("tmp")
.arg(
Arg::new("pat")
.allow_hyphen_values(true)
.required(true)
.takes_value(true),
)
.get_matches_from(["tmp", "-file"]);
assert_eq!(m.value_of("pat"), Some("-file"));
}
#[test]
fn issue_1093_allow_ext_sc() {
let app = App::new("clap-test")