diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b13b4984..55e11d4d5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ------------------------ diff --git a/doc_src/cmds/argparse.rst b/doc_src/cmds/argparse.rst index 0ce738a4b..becc7a675 100644 --- a/doc_src/cmds/argparse.rst +++ b/doc_src/cmds/argparse.rst @@ -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 ` 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 ` section below. Each option specification (``OPTION_SPEC``) is written in the :ref:`domain specific language ` 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 ----------- diff --git a/src/builtins/argparse.rs b/src/builtins/argparse.rs index b3caf6ba2..cb956eed6 100644 --- a/src/builtins/argparse.rs +++ b/src/builtins/argparse.rs @@ -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>, long_to_short_flag: HashMap, exclusive_flag_sets: Vec>, @@ -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 diff --git a/src/wgetopt.rs b/src/wgetopt.rs index aec05b8c8..15ab0e8e4 100644 --- a/src/wgetopt.rs +++ b/src/wgetopt.rs @@ -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(); diff --git a/tests/checks/argparse.fish b/tests/checks/argparse.fish index 60ee8229d..f4d800e06 100644 --- a/tests/checks/argparse.fish +++ b/tests/checks/argparse.fish @@ -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