Make argparse save parsed options in $argv_opts. (Fixes #6466)

Specifically, every argument (other than the first --, if any) that argparse
doesn't add to $argv is now added to a new local variable $argv_opts. This
allows you to make wrapper commands that modify non-option arguments, and then
forwards all arguments to another command. See the new example at the end of
doc_src/cmds/argparse.rst for a use case for this new variable.
This commit is contained in:
Isaac Oscar Gariano
2025-08-03 10:07:08 +10:00
parent e6b4f0a696
commit e62abc460d
5 changed files with 89 additions and 7 deletions

View File

@@ -42,6 +42,7 @@ Deprecations and removed features
Scripting improvements
----------------------
- The ``psub`` command now allows combining ``--suffix`` with ``--fifo`` (:issue:`11729`).
- ``argparse`` now saves recognised options and values in ``$argv_opts``, allowing them to be forwarded to other commands (:issue:`6466`).
Interactive improvements
------------------------

View File

@@ -14,7 +14,7 @@ Synopsis
Description
-----------
This command makes it easy for fish scripts and functions to handle arguments. You pass arguments that define the known options, followed by a literal **--**, then the arguments to be parsed (which might also include a literal **--**). ``argparse`` then sets variables to indicate the passed options with their values, and sets ``$argv`` to the remaining arguments. See the :ref:`usage <cmd-argparse-usage>` section below.
This command makes it easy for fish scripts and functions to handle arguments. You pass arguments that define the known options, followed by a literal **--**, then the arguments to be parsed (which might also include a literal **--**). ``argparse`` then sets variables to indicate the passed options with their values, sets ``$argv_opts`` to the options and their values, and sets ``$argv`` to the remaining arguments. See the :ref:`usage <cmd-argparse-usage>` section below.
Each option specification (``OPTION_SPEC``) is written in the :ref:`domain specific language <cmd-argparse-option-specification>` described below. All OPTION_SPECs must appear after any argparse flags and before the ``--`` that separates them from the arguments to be parsed.
@@ -217,7 +217,7 @@ Some *OPTION_SPEC* examples:
- ``#longonly`` causes the last integer option to be stored in ``_flag_longonly``.
After parsing the arguments the ``argv`` variable is set with local scope to any values not already consumed during flag processing. If there are no unbound values the variable is set but ``count $argv`` will be zero.
After parsing the arguments the ``argv`` variable is set with local scope to any values not already consumed during flag processing. If there are no unbound values the variable is set but ``count $argv`` will be zero. Similarly, the ``argv_opts`` variable is set with local scope to the arguments that *were* consumed during flag processing. This allows forwarding ``$argv_opts`` to another command, together with additional arguments.
If an error occurs during argparse processing it will exit with a non-zero status and print error messages to stderr.
@@ -259,6 +259,48 @@ After this it figures out which variable it should operate on according to the `
and set $var $result
An example of using ``$argv_opts`` to forward known options to another command, whilst adding new options::
function my-head
# The following options are existing ones to head that we will forward verbatim
set -l opt_spec n/lines= q/quiet silent v/verbose z/zero-terminated help version
argparse --ignore-unknown $opt_spec -- $argv || return
set -l opts $argv_opts # Save it so it isn't overridden by the next argparse call
# --qwords is a new option, but --bytes is an existing one which we will modify below
argparse qwords= c/bytes= -- $argv || return
if set -q _flag_qwords
# --qwords allows specifying the size in multiples of 8 bytes
set -a opts --bytes=(math -- $_flag_qwords \* 8 || return)
else if set -q _flag_bytes
# Allows using a 'q' suffix, e.g. --bytes=4q to mean 4*8 bytes.
if string match -qr 'q$' -- $_flag_bytes
set -a opts --bytes=(math -- (string replace -r 'q$' '*8' -- $_flag_bytes) || return)
else
# Keep the users setting
set -a opts --bytes=$_flag_bytes
end
end
if test (count $argv) -eq 0
# Default to heading /dev/kmsg (whereas head defaults to stdin)
set -l argv /dev/kmsg
end
# Call the real head with our modified options and arguments.
head $opts -- $argv
end
The first argparse call above saves all the options we do *not* want to process in ``$argv_opts``, which we then copy in to ``$opts``.
The second `argparse` call then parses the options we *do* want to process. The new
value of ``$argv_opts`` is ignored, and instead the ``$_flag_OPTION`` variables are used to transform each of these additional options and
add them back to ``$opts``.
Note that the first ``argparse`` call is *needed* for the code that inspects ``$argv`` to work, as it checks for the absence of any *non*-option arguments (i.e. no file was specified). We'd similarly need the first call if we wanted to modify the given filenames.
Limitations
-----------

View File

@@ -59,6 +59,7 @@ struct ArgParseCmdOpts<'args> {
name: WString,
raw_exclusive_flags: Vec<&'args wstr>,
args: Vec<&'args wstr>,
args_opts: Vec<&'args wstr>,
options: HashMap<char, OptionSpec<'args>>,
long_to_short_flag: HashMap<WString, char>,
exclusive_flag_sets: Vec<Vec<char>>,
@@ -792,6 +793,7 @@ fn argparse_parse_flags<'args>(
// This allows reusing the same argv in multiple argparse calls,
// or just ignoring the error (e.g. in completions).
opts.args.push(args_read[w.wopt_index - 1]);
w.argv_opts.pop();
// Work around weirdness with wgetopt, which crashes if we `continue` here.
if w.wopt_index == argc {
break;
@@ -816,6 +818,7 @@ fn argparse_parse_flags<'args>(
};
retval?;
}
opts.args_opts = w.argv_opts;
*optind = w.wopt_index;
return Ok(SUCCESS);
@@ -901,6 +904,8 @@ fn set_argparse_result_vars(vars: &EnvStack, opts: &ArgParseCmdOpts) {
let args = opts.args.iter().map(|&s| s.to_owned()).collect();
vars.set(L!("argv"), EnvMode::LOCAL, args);
let args_opts = opts.args_opts.iter().map(|&s| s.to_owned()).collect();
vars.set(L!("argv_opts"), EnvMode::LOCAL, args_opts);
}
/// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this

View File

@@ -139,6 +139,9 @@ pub struct WGetopter<'opts, 'args, 'argarray> {
return_colon: bool,
/// Prevents redundant initialization.
initialized: bool,
/// This will be populated with the elements of the original args that were interpreted
/// as options and arguments to options
pub argv_opts: Vec<&'args wstr>,
}
impl<'opts, 'args, 'argarray> WGetopter<'opts, 'args, 'argarray> {
@@ -160,6 +163,7 @@ pub fn new(
last_nonopt: 0,
return_colon: false,
initialized: false,
argv_opts: Vec::new(),
}
}
@@ -302,14 +306,17 @@ fn next_argv(&mut self) -> NextArgv {
return NextArgv::UnpermutedNonOption;
}
let opt = self.argv[self.wopt_index];
self.argv_opts.push(opt);
// We've found an option, so we need to skip the initial punctuation.
let skip = if !self.longopts.is_empty() && self.argv[self.wopt_index].char_at(1) == '-' {
let skip = if !self.longopts.is_empty() && opt.char_at(1) == '-' {
2
} else {
1
};
self.remaining_text = self.argv[self.wopt_index][skip..].into();
self.remaining_text = opt[skip..].into();
NextArgv::FoundOption
}
@@ -365,7 +372,9 @@ fn handle_short_opt(&mut self) -> char {
c = if self.return_colon { ':' } else { '?' };
} else {
// Consume the next element.
self.woptarg = Some(self.argv[self.wopt_index]);
let val = self.argv[self.wopt_index];
self.argv_opts.push(val);
self.woptarg = Some(val);
self.wopt_index += 1;
}
}
@@ -393,7 +402,9 @@ fn update_long_opt(
}
} else if opt_found.arg_type == ArgType::RequiredArgument {
if self.wopt_index < self.argv.len() {
self.woptarg = Some(self.argv[self.wopt_index]);
let val = self.argv[self.wopt_index];
self.argv_opts.push(val);
self.woptarg = Some(val);
self.wopt_index += 1;
} else {
self.remaining_text = empty_wstr();

View File

@@ -31,6 +31,7 @@ begin
# CHECK: 0
set -l
# CHECK: argv hello
# CHECK: argv_opts
end
# Invalid option specs
@@ -154,6 +155,7 @@ begin
argparse h/help -- help
set -l
# CHECK: argv help
# CHECK: argv_opts
end
# Five args with two matching a flag
@@ -163,12 +165,13 @@ begin
# CHECK: _flag_h '--help' '-h'
# CHECK: _flag_help '--help' '-h'
# CHECK: argv 'help' 'me' 'a lot more'
# CHECK: argv_opts '--help' '-h'
end
# Required, optional, and multiple flags
begin
argparse h/help 'a/abc=' 'd/def=?' 'g/ghk=+' -- help --help me --ghk=g1 --abc=ABC --ghk g2 -d -g g3
set -l
set -lL
# CHECK: _flag_a ABC
# CHECK: _flag_abc ABC
# CHECK: _flag_d
@@ -178,6 +181,7 @@ begin
# CHECK: _flag_h --help
# CHECK: _flag_help --help
# CHECK: argv 'help' 'me'
# CHECK: argv_opts '--help' '--ghk=g1' '--abc=ABC' '--ghk' 'g2' '-d' '-g' 'g3'
end
# --stop-nonopt works
@@ -189,6 +193,7 @@ begin
# CHECK: _flag_h -h
# CHECK: _flag_help -h
# CHECK: argv 'non-opt' 'second non-opt' '--help'
# CHECK: argv_opts '-a' 'A1' '-h' '--abc' 'A2'
end
# Implicit int flags work
@@ -197,6 +202,7 @@ begin
set -l
# CHECK: _flag_val 123
# CHECK: argv 'abc' 'def'
# CHECK: argv_opts -123
end
begin
argparse v/verbose '#-val' 't/token=' -- -123 a1 --token woohoo --234 -v a2 --verbose
@@ -207,6 +213,7 @@ begin
# CHECK: _flag_val -234
# CHECK: _flag_verbose '-v' '--verbose'
# CHECK: argv 'a1' 'a2'
# CHECK: argv_opts '-123' '--token' 'woohoo' '--234' '-v' '--verbose'
end
# Should be set to 987
@@ -216,6 +223,7 @@ begin
# CHECK: _flag_m 987
# CHECK: _flag_max 987
# CHECK: argv 'argle' 'bargle'
# CHECK: argv_opts -987
end
# Should be set to 765
@@ -225,6 +233,7 @@ begin
# CHECK: _flag_m 765
# CHECK: _flag_max 765
# CHECK: argv 'argle' 'bargle'
# CHECK: argv_opts '-987' '--max' '765'
end
# Bool short flag only
@@ -234,6 +243,7 @@ begin
# CHECK: _flag_C -C
# CHECK: _flag_v '-v' '-v'
# CHECK: argv 'arg1' 'arg2'
# CHECK: argv_opts '-C' '-v' '-v'
end
# Value taking short flag only
@@ -244,6 +254,7 @@ begin
# CHECK: _flag_verbose '--verbose' '-v'
# CHECK: _flag_x arg2
# CHECK: argv arg1
# CHECK: argv_opts '--verbose' '-v' '-x' 'arg2'
end
# Implicit int short flag only
@@ -254,6 +265,7 @@ begin
# CHECK: _flag_verbose '-v' '-v' '-v'
# CHECK: _flag_x 321
# CHECK: argv 'argle' 'bargle'
# CHECK: argv_opts '-v' '-v' '-v' '-x' '321'
end
# Implicit int short flag only with custom validation passes
@@ -264,6 +276,7 @@ begin
# CHECK: _flag_verbose '-v' '-v' '-v'
# CHECK: _flag_x 499
# CHECK: argv
# CHECK: argv_opts '-v' '-v' '-x' '499' '-v'
end
# Implicit int short flag only with custom validation fails
@@ -326,8 +339,10 @@ begin
or echo unexpected argparse return status $status >&2
# The unknown options are removed _entirely_.
echo $argv
echo $argv_opts
echo $_flag_a
# CHECK: -t tango --wurst
# CHECK: -a alpha -b bravo -a aaaa
# CHECK: alpha aaaa
end
@@ -338,6 +353,7 @@ begin
# CHECK: _flag_b -b
# CHECK: _flag_break -b
# CHECK: argv '-b kubectl get pods -l name=foo'
# CHECK: argv_opts
end
begin
@@ -346,7 +362,9 @@ begin
printf '%s\n' $argv
# CHECK: a
# CHECK: b
printf '%s\n' $argv_opts
# CHECK: -a
# CHECK: --alpha
end
begin
@@ -361,6 +379,7 @@ begin
# CHECK: _flag_i 2
# CHECK: _flag_o 3
# CHECK: argv
# CHECK: argv_opts '-i' '2' '-o' '3'
end
# long-only flags
@@ -370,6 +389,7 @@ begin
# CHECK: _flag_foo --foo
# CHECK: _flag_installed no
# CHECK: argv
# CHECK: argv_opts '--installed=no' '--foo'
end
begin
@@ -378,6 +398,7 @@ begin
# CHECK: _flag_foo --foo
# CHECK: _flag_installed 5
# CHECK: argv
# CHECK: argv_opts '--installed=5' '--foo'
end
begin
@@ -386,6 +407,7 @@ begin
# CHECK: _flag_installed 5
# CHECK: _flag_num 5
# CHECK: argv
# CHECK: argv_opts '--installed=5' '-5'
end
begin
@@ -498,6 +520,7 @@ begin
argparse --ignore-unknown h i -- -hoa -oia
echo -- $argv
#CHECK: -hoa -oia
echo -- $argv_opts
echo $_flag_h
#CHECK: -h
set -q _flag_i