-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Variadic and strict function --argument-names
#11780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This simply does the same thing as fish-shell#10948, but for the function builtin.
This makes it so that printing a function definition will only use one --argument-names group, instead of one for argument name. For example, "function foo -a x y; ..." will print with "function foo --argument-names x y" instead of "function foo --argument-names x --argument-names y", which is very bizarre. Moreover, the documentation no longer says that argument-names "Has to be the last option.". This sentence appears to have been introduced in error by pull fish-shell#10524, since the ability to have options afterwards was deliberately added by pull fish-shell#6188.
Previously, if you called a function parameter 'argv', within the body of the function, argv would be set to *all* the arguments to the function, and not the one indicated by the parameter name. The same behaviour happened if you inherited a variable named 'argv'. Both behaviours were quite surprising, so this commit makes things more obvious, although they could alternatively simply be made errors.
It is now an error to use the same variable name twice with
--inherit-variable and/pr --argument-names.
The previous behaviour was:
1. duplicate --inherit-variable's worked as if you only inherited
the variable once
2. Later --argument-names override earlier ones
3. An --inherit-variable overrides an --argument-names
Thus this change is backwards incompatible, however, such duplicate
names are not useful and where likely unintentional, and none of the
test cases were depending on the old behaviour.
(I do note that there was code written to specifically remove any
duplicate --inherit-variable's, but the only difference this made was
that the duplicates wouldn't be displayed by the functions or type
builtin. This seems to have been done to preserve the prior behaviour
when it was a HashMap.)
Specifically, this adds a section to the "function" builtin documentation explaining what happens when you call a function defined with --argument-names, but with a different number of arguments than argument-names.
Specifically, this allows the last --argument-names in a function definition to be suffixed with ... This causes any leftover arguments in argv to be appended to the affixed variable name. The documentation has been updating giving details on how different numbers of argument names and argv elements are reconciled, with and without the new ...
This commit makes it so that an argument-name ending with a ... in a function's --argument-names list is not required to be the last. There can still only be one such argument name. The idea is that a '...' argument name will consume as many arguments from argv as possible, whilst still leaving one argument for each normal argument name (if possible).
The new -A/--strict-argument-names option behave like the existing
-a/--argument-names option to function, but with the following
differences:
- If none of the argument names contains a ... suffix, it will be
an error to call the function with a different number of arguments
compared to the number of argument names.
- If one of the argument names does contain a ... suffix, it will
be an error to call it with less arguments, than non-... argument
names.
- it cannot be used together with -a/--argument-names (e.g.
function f -A x -A y, and function f -a x -a y are allowed, but
function f -A x -a y is not.)
- It can be used without any argument argument names
(e.g. function f -A), indicating that the function takes no arguments.
An error is produced as follows:
- An error message of the following form is printed to stderr:
error: <name>: function requires exactly <n> arguments, but <m> where supplied
- The function is not called
- A status code of 2 is returned (similarly to other builtins
when you provide them with an incorrect number/format of arguments)
Note that the error message is printed *before* the function is called,
so any redirections on the function call statement are not applied,
this is a bug that should be fixed.
This new feature fixes fish-shell#11728.
| The following options are available: | ||
|
|
||
| **-a** *NAMES* or **--argument-names** *NAMES* | ||
| Has to be the last option. Assigns the value of successive command-line arguments to the names given in *NAMES* (separated by space). These are the same arguments given in :envvar:`argv`, and are still available there. See also :ref:`Argument Handling <variables-argv>`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually do one-sentence-per line to reduce noise in line-based diffs.
Maybe that's an anachronistic perspective, there might be disadvantages to one-sentence-per line, I'm not sure
|
|
||
| **-a** *NAMES* or **--argument-names** *NAMES* | ||
| Has to be the last option. Assigns the value of successive command-line arguments to the names given in *NAMES* (separated by space). These are the same arguments given in :envvar:`argv`, and are still available there. See also :ref:`Argument Handling <variables-argv>`. | ||
| Assigns the value of successive command-line arguments to the names given in *NAMES* (separated by spaces). These are the same arguments given in :envvar:`argv`, and are still available there (unless ``--inherit-variable argv`` was used or one of the given *NAMES* is ``argv``). See also :ref:`Argument Handling <variables-argv>`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first reaction to cd8c938 (Allow overwriting argv with function -a and -V, 2025-08-22)
was: yes, we can do this, but is it actually useful?
If it's a user error 90% of the time, then we should rather throw an error when creating the function.
I guess "--argument-names head argv" can be an elegant way to write recursive functions,
it communicates early that we don't care about the full $argv anymore.
So this looks fine to me, same for -V.
| Assigns the value of successive command-line arguments to the names given in *NAMES* (separated by spaces). These are the same arguments given in :envvar:`argv`, and are still available there (unless ``--inherit-variable argv`` was used or one of the given *NAMES* is ``argv``). See also :ref:`Argument Handling <variables-argv>`. | ||
|
|
||
| A single argument name given can end in ``...``, in which case a variable number of arguments are saved in a variable with that name (excluding the ``...`` part), as many as possible whilst still leaving arguments for each of the other named arguments. If this cannot be done (because the number of arguments is smaller than the number of argument names), the argument with a ``...`` is set to the empty list, and the other arguments are asigned values as if the ``...`` one was not there. | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the error message produced by -A cannot be redirected away, e.g.:
function foo -A arg; end
foo &>/dev/null # Still prints an error message
However, this is consistent with the behaviour of calling a non-existent function/command.
huh this works in bash, but not in fish.
It also affects the time keyword (also in bash), for better or worse.
I'm not sure if we should change the behavior (for non-existent commands),
but the way it works is that fish has an IoChain passed along as execution context.
Before fish spawns a process, it resolves the IoChain to file descriptors,
and passes those to the forked process (or to the builtin function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can find how to do this later, if we actually keep this approach
| See the :ref:`Argument Names Caveats <argument_names_caveats>` section below for what happens when the number of arguments passed differs from the number of argument *NAMES*. | ||
|
|
||
| **-A** *NAMES* or **--strict-argument-names** *NAMES* | ||
| This behaves like like ``-a`` / ``--argument-names``, except that calling the function with an incorrect number of arguments will print an error, return 2, and not execute the body of the function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not yet 100% sure if we should add this feature.
The weird part here is that it means that function calls
may print an error before entering the actual function body.
I don't know if it's an actual problem in practice, but
function authors might want control of their error messages.
But of course, they don't need to use this flag.
And everyone is fine with using argparse too..
I think another potential problem with growing function options like this is that it will prevent some people from discovering (or taking the plunge into) argparse,
which seems strictly superior (we might need to add --argument-names).
When I write CLI tools (mainly POSIX shell scripts), I often use
environment variables, or positional arguments because I'm too lazy to
figure out how to use getopts. I think it's much easier with argparse,
and that leads to nicer CLIs.
I think @Elec3137's concern in #11728 about argparse was that they didn't want to specify args redundantly. That we can achieve with argparse
function getCreationTime --description 'outputs the creation time of the given subvolume'
argparse --positionals subvol -- $argv
or return
...
end
Note that they didn't use return but exit 1 (i.e. exit the whole shell, or, if inside source, exit the current file).
If they actually want exit semantics rather than only from returning from the current function,
then this --strict-argument-names feature wouldn't help.
So let's make sure we have a sound and complete usage example.
I'd argue that the argparse example above is pretty nice;
explicit return is how things are currently done,
and the separate line doesn't hurt. Not sure.
So let me know if you think any of my changes would not be suitable for argparse or set.
Ok, let's think about that.
Either way, so far I'm happy with all the commits leading up to the last two.
Can you drop these from this PR, and maybe move them to separate ones? (if there are conflicts between the two, then one PR is fine)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think another potential problem with growing
functionoptions like this is that it will prevent some people from discovering (or taking the plunge into) argparse,
which seems strictly superior (we might need to add--argument-names).
I am in agreement with keeping argparse as our preferred approach
| Argument Names Caveats | ||
| ---------------------- | ||
|
|
||
| The ``-a`` / ``--argument-names`` flag does *not* validate the number of arguments passed: if an argument is missing the corresponding variable will be assigned to an empty list. For example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the corresponding variable will be assigned to an empty list.
I think it should be "will be assigned to empty list", (no "to"),
or "an empty list will be assigned to the variable"
| parser: &Parser, | ||
| streams: &mut IoStreams, | ||
| ) -> c_int { | ||
| ) -> BuiltinResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This simply does the same thing as #10948, but for the function builtin.
FWIW, I personally prefer to see commit references (e.g. "git log
-1 --pretty=reference") rather than issue/PR numbers, mainly because
then I don't need GitHub-specific tools to follow the reference.
| return STATUS_INVALID_ARGS; | ||
| } | ||
| opts.named_arguments.push(woptarg); | ||
| proccess_argument_name(opts, woptarg, streams)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't read the context, but maybe a better name for this function is add_argument_name(), to make it obvious we're adding to opts.
(unless we fail, but that's also obvious).
It's fine to leave, your call
| for named in opts.inherit_vars.drain(..) { | ||
| if !inherit_vars.insert(named.clone()) { | ||
| streams.err.append(wgettext_fmt!( | ||
| "%ls: variable '%ls' is inherited multiple times\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the name might be misleading it first because we (obviously) refuse to have it inherit anything in this case.
Not sure if there is a better way to phrase it. Maybe
%ls: cannot inherit variable '%ls' multiple times
%ls: duplicate variable '%ls' in --inherit-variable
The second one would be consistent with --argument-names, but -V arguments are disjoint, so it's not the same.
Not sure. Your call
| props.annotated_definition(arg) | ||
| )); | ||
| let defy = props.annotated_definition(arg); | ||
| def.push_utfstr(&sprintf!("# %ls\n%ls", comment, defy,)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's a slightly weird variable name (though it seems okay) and a bad trailing comma
| #CHECKERR: {{.*}}checks/function.fish (line {{\d+}}): function: variable 'var' is passed to both --argument-names and --inherit-variable | ||
| #CHECKERR: function bad -a var var -V var | ||
| #CHECKERR: ^ | ||
| not functions -q bad || echo "function should not exist" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, this originally called "bad" in c813ea2 (Prohibit duplicate
variable names with function -a and -V options, 2025-08-22), which we
shouldn't to because a fish dev might have a command with that name in
$PATH. The not functions -q bad check looks perfect; ideally
it should have been squashed to make it easier to review
|
cherry-picking the first 3 commits, still want some of the others |
This simply does the same thing as fish-shell#10948, but for the function builtin. Part of fish-shell#11780
This makes it so that printing a function definition will only use one --argument-names group, instead of one for argument name. For example, "function foo -a x y; ..." will print with "function foo --argument-names x y" instead of "function foo --argument-names x --argument-names y", which is very bizarre. Moreover, the documentation no longer says that argument-names "Has to be the last option.". This sentence appears to have been introduced in error by pull fish-shell#10524, since the ability to have options afterwards was deliberately added by pull fish-shell#6188. Part of fish-shell#11780
Previously, if you called a function parameter 'argv', within the body of the function, argv would be set to *all* the arguments to the function, and not the one indicated by the parameter name. The same behaviour happened if you inherited a variable named 'argv'. Both behaviours were quite surprising, so this commit makes things more obvious, although they could alternatively simply be made errors. Part of fish-shell#11780
This pull request adds two new features (as discussed in issue #11728):
functions-a/--argument-nameslist can end in..., the variable (with the...removed) will be set to a list containing as many arguments as possible, whilst still leaving arguments to be assigned to the rest of the parameters.-A/--strict-argument-namesoption is added that works like-a/--argument-names, but you will get an error if you use the function with an incorrect number of arguments.I also 'fixed' a few strange related behaviours that I noticed (see below).
I've documented everything and added test cases, however there is one bug (in my last commit): the error message produced by
-Acannot be redirected away, e.g.:However, this is consistent with the behaviour of calling a non-existent function/command.
Please let me know if you have any idea how to fix this.
Details
Copied from the documentation for
function, with changed behaviour in bold:-a NAMESor--argument-names NAMESAssigns the value of successive command-line arguments to the names given in NAMES (separated by spaces). These are the same arguments given in
argv, and are still available there (unless--inherit-variable argvwas used or one of the given NAMES isargv). See also Argument Handling.A single argument name given can end in
..., in which case a variable number of arguments are saved in a variable with that name (excluding the...part), as many as possible whilst still leaving arguments for each of the other named arguments. If this cannot be done (because the number of arguments is smaller than the number of argument names), the argument with a...is set to the empty list, and the other arguments are asigned values as if the...one was not there.See the Argument Names Caveats section below for what happens when the number of arguments passed differs from the number of argument NAMES.
-A NAMESor--strict-argument-names NAMESThis behaves like like
-a/--argument-names, except that calling the function with an incorrect number of arguments will print an error, return 2, and not execute the body of the function.If none of the given NAMES use a
..., this error will trigger when the number of actual arguments does not equal the number of NAMES.Otherwise, (if
...was used) the error will trigger if the number of actual arguments is less than the number NAMES minus 1.Unlike the
-a/--argument-namesoption, the given NAMES list can be empty, in which case an error will be generated if the function is called with a non-empty list of arguments.Argument Names Caveats
(this section is new, but some of it describes existing behaviour)
The
-a/--argument-namesflag does not validate the number of arguments passed: if an argument is missing the corresponding variable will be assigned to an empty list. For example:In contrast, if the
-A/--strict-argument-namesoption where used instead, the above call totwowould produce an error.Similarly, if
-a/--argument-namesis used and none of the argument names end in..., any extra arguments are ignored, but they are still accessible in$argv. Continuing the previous example:Using
-A/--strict-argument-nameswill make the above call totwoan error.If on the other hand the last argument name does end in
..., any extra arguments are stored in that variable. For example:The same behaviour occurs with the
-A/--strict-argument-namesoption.If the argument named with a
...is not the last, then if possible, it will leave enough arguments for the subsequent argument names. For example:The argument with a
...is however set to the empty list if there are not enough arguments to satisfy all the argument names. For example:However, with the
-A/--strict-argument-namesoption, the above call tomore_or_twowould produce an error.The Commits
This first group of changes are not necessary for the rest of the functionality, so I'm happy to remove them if you don't like them:
f7f60b3 Use idiomatic Rust error handling for function builtin.
6a7667d Output function argument-names in one group.
cd8c938 Allow overwriting argv with function -a and -V
c813ea2 Prohibit duplicate variable names with function -a and -V options
The next commit adds a section to the documentation that the remaining commits add to:
4ee163d Added more documentation for function --argument-names
6ad3d9e Allow variadic function --argument-names
181c8f9 Do not require the variadic --argument-names to be the last one (note: I made this a seperate commit in case it's too strange/useless)
25a79d4 Add an -A/--strict-argument-names option to function
Future Stuff
If this pull request is accepted, I would like to add:
-a/--argument-namesand-A/--strict-argument-namestoargparse(although I note similar suggestions were previously rejected in We should be able to specify an array that argparse will alter/set #5778 and Enhance argparse with option to set variable names. #5946, but they didn't include my new...feature).-m/--multipleand-M/--strict-multipletoset(fixing Add option tosetfor unwrapping, i.e. splitting and assigning to two multiple variables #11457), that would be similar to-aand-A, the usage would be:<options>are any existing set options that make sense. Each<var>part could be anything that set currently supports (e.g.foo[1 3]), or a name followed by....So let me know if you think any of my changes would not be suitable for
argparseorset.