Skip to content

Conversation

@IsaacOscar
Copy link
Contributor

This pull request adds two new features (as discussed in issue #11728):

  1. A single argument name in a functions -a/--argument-names list 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.
  2. A new -A/--strict-argument-names option 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 -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.
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 NAMES or --argument-names NAMES
    Assigns 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 argv was used or one of the given NAMES is argv). 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 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.
    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-names option, 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-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:

function two -a x y
    set -l
end
two 1
# prints:
#    argv 1
#    x 1
#    y

In contrast, if the -A / --strict-argument-names option where used instead, the above call to two would produce an error.

Similarly, if -a / --argument-names is 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:

two 1 2 3
# prints:
#    argv '1'  '2'  '3'
#    x 1
#    y 2

Using -A / --strict-argument-names will make the above call to two an error.

If on the other hand the last argument name does end in ..., any extra arguments are stored in that variable. For example:

function one_or_more -a x y...
    set -l
end
one_or_more 1 2 3
# prints:
#    argv '1'  '2'  '3'
#    x 1
#    y '2'  '3'

The same behaviour occurs with the -A / --strict-argument-names option.

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:

function more_or_one -a x... y
    set -l
end
more_or_one 1 2 3
# prints:
#    argv '1'  '2'  '3'
#    x '1'  '2'
#    y 3

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:

function more_or_two -a x... y z
    set -l
end
more_or_two 1
# prints:
#    argv 1
#    x
#    y 1
#    z

However, with the -A / --strict-argument-names option, the above call to more_or_two would 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:

  1. f7f60b3 Use idiomatic Rust error handling for function builtin.

    This simply does the same thing as Convert builtin functions return type from Option<c_int> to Result<StatusOk, StatusError>. #10948, but for the function builtin.

  2. 6a7667d Output function argument-names in one group.

    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 Explain function --argument-names in more detail. #10524, since the ability to have options afterwards was
    deliberately added by pull Fix options after --argument-names to function #6188.

  3. cd8c938 Allow overwriting argv with function -a and -V

    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.

  4. c813ea2 Prohibit duplicate variable names with function -a and -V options

    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.)

The next commit adds a section to the documentation that the remaining commits add to:

  1. 4ee163d Added more documentation for function --argument-names

    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.

  2. 6ad3d9e Allow variadic function --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 ...

  3. 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)

    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).

  4. 25a79d4 Add an -A/--strict-argument-names option to function

    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: : function requires exactly arguments, but 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 make functions defined in fish spit error on invalid argument count (suggestion/enhancement) #11728.

Future Stuff

If this pull request is accepted, I would like to add:

So let me know if you think any of my changes would not be suitable for argparse or set.

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>`.
Copy link
Contributor

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>`.
Copy link
Contributor

@krobelus krobelus Sep 14, 2025

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.

Copy link
Contributor

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).

Copy link
Contributor

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.
Copy link
Contributor

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)

Copy link
Member

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 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).

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:
Copy link
Contributor

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 {
Copy link
Contributor

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)?;
Copy link
Contributor

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",
Copy link
Contributor

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,));
Copy link
Contributor

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"
Copy link
Contributor

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

@krobelus
Copy link
Contributor

krobelus commented Oct 11, 2025

cherry-picking the first 3 commits, still want some of the others

krobelus pushed a commit to krobelus/fish-shell that referenced this pull request Oct 11, 2025
This simply does the same thing as fish-shell#10948, but for the function builtin.

Part of fish-shell#11780
krobelus pushed a commit to krobelus/fish-shell that referenced this pull request Oct 11, 2025
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
krobelus pushed a commit to krobelus/fish-shell that referenced this pull request Oct 11, 2025
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make functions defined in fish spit error on invalid argument count (suggestion/enhancement)

3 participants