cli: deal with commands without positional args

When reaching the final command we relied on the positional parameters
"ending" the global option parsing. This means we did not get global
options at the end, and failed with "unknown option" instead.
Fix this by simply retaining unknown options in that case and not
stopping at positional parameters.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2024-07-19 13:50:00 +02:00
parent fee00addab
commit 5eb7cbd250
2 changed files with 29 additions and 9 deletions

View File

@ -263,6 +263,7 @@ pub(crate) struct ParseOptions<'t, 'o> {
stop_at_positional: bool,
stop_at_unknown: bool,
deny_unknown: bool,
retain_unknown: bool,
retain_separator: bool,
}
@ -278,6 +279,7 @@ impl<'t, 'o> ParseOptions<'t, 'o> {
stop_at_positional: false,
stop_at_unknown: false,
deny_unknown: false,
retain_unknown: false,
retain_separator: false,
}
}
@ -288,6 +290,12 @@ impl<'t, 'o> ParseOptions<'t, 'o> {
self
}
/// Builder style option to retain unknown parameters in the returned argument array.
pub fn retain_unknown(mut self, deny: bool) -> Self {
self.retain_unknown = deny;
self
}
/// Builder style option to stop parsing on unknown parameters.
/// This implies deny_unknown`.
/// Useful for bash completion.
@ -351,13 +359,20 @@ impl<'t, 'o> ParseOptions<'t, 'o> {
let opt_schema = self.option_schemas.get(option).copied();
if self.deny_unknown && opt_schema.is_none() {
if opt_schema.is_none() {
if self.retain_unknown {
positional.push(orig_arg);
continue;
}
if self.deny_unknown {
if self.stop_at_unknown {
positional.push(orig_arg);
break;
}
errors.push(option.to_string(), format_err!("unknown option {option:?}"));
}
}
if let Some(value) = value {
self.target.push((option.to_string(), value.to_string()));

View File

@ -464,11 +464,16 @@ impl<'cli> CommandLineParseState<'cli> {
}
/// Parse out the current global options and return the remaining `args`.
fn handle_current_global_options(&mut self, args: Vec<String>) -> Result<Vec<String>, Error> {
fn handle_current_global_options(
&mut self,
args: Vec<String>,
needs_subcommand: bool,
) -> Result<Vec<String>, Error> {
let mut global_args = Vec::new();
let args = getopts::ParseOptions::new(&mut global_args, &self.global_option_schemas)
.stop_at_positional(true)
.deny_unknown(true)
.deny_unknown(needs_subcommand)
.stop_at_positional(needs_subcommand)
.retain_unknown(!needs_subcommand)
.parse(args)?;
// and merge them into the hash map
for (option, argument) in global_args {
@ -505,7 +510,7 @@ impl<'cli> CommandLineParseState<'cli> {
self.enable_global_options(cli);
let mut args = self.handle_current_global_options(args)?;
let mut args = self.handle_current_global_options(args, true)?;
// now deal with the actual subcommand list
if args.is_empty() {
@ -538,7 +543,7 @@ impl<'cli> CommandLineParseState<'cli> {
rpcenv: &mut CliEnvironment,
args: Vec<String>,
) -> Result<Invocation<'cli>, Error> {
let args = self.handle_current_global_options(args)?;
let args = self.handle_current_global_options(args, false)?;
self.build_global_options(&mut *rpcenv)?;
let interface = Arc::clone(&self.interface);
Ok(Invocation {