Allow optional argument(s) to precede mandatory argument(s)
I would like to be able to define a command that takes two arguments, of which the first is optional, like this:
import click
@click.command()
@click.argument("foo", required=False)
@click.argument("bar", required=True)
def main(*, bar: str, foo: str | None = None) -> None:
click.echo(f"foo={foo!r} bar={bar!r}")
if __name__ == "__main__": main()
This program behaves as expected when two arguments are provided, and the --help output of this program correctly reflects my intent:
$ python3 test.py --help
Usage: test.py [OPTIONS] [FOO] BAR
Options:
--help Show this message and exit.
The error message when no arguments are provided is also correct as is:
$ python3 test.py
Usage: test.py [OPTIONS] [FOO] BAR
Try 'test.py --help' for help.
Error: Missing argument 'BAR'.
However, when only one argument is provided, it is assigned to 'foo' rather than 'bar' and so you get an error again:
$ python3 test.py a
Usage: test.py [OPTIONS] [FOO] BAR
Try 'test.py --help' for help.
Error: Missing argument 'BAR'.
Instead, this invocation should set 'foo' to None and 'bar' to "b" and execute the command.
Note that click does currently support putting a zero-or-more argument before a required argument, and this usage appears in the official inout example, so I presume it's on purpose. This slight variation on the example above works as expected:
import click
@click.command()
@click.argument("foo", nargs=-1)
@click.argument("bar", required=True)
def fn(*, bar: str, foo: list[str]) -> None:
click.echo(f"foo={foo!r} bar={bar!r}")
fn()
-->
$ python3 test2.py a
foo=() bar='a'
$ python3 test.py a b
foo=('a',) bar='b'
$ python3 test.py a b c
foo=('a', 'b') bar='c'
So, not being able to do the same with a zero-or-one argument followed by a required argument seems inconsistent.
Here is an excerpt of help text for the program that motivated me to want this feature:
mast-upload check-label [<label.yml>] <directory>
This mode assists you with the process of finalizing the label.
It checks for syntax errors, missing metadata, files in the data
set directory that aren’t covered by the label or explicitly marked as
excluded from the data set, files that don’t match what the label says
about them, etc. Unlike `mast-upload scan` this mode will not modify
the label. If you give only a directory on the command line, it will
look for `CONTENTS.YML` in the top level of the directory.
Putting the "label.yml" argument after the "directory" argument would confuse people into thinking that the directory is being checked against the label, rather than the reality that the label is being checked against the directory.
It is possible to work around the absence of this feature by shuffling parameters around after the fact in the command body:
import click
@click.command()
@click.argument("bar_or_foo", metavar="[FOO]", required=True)
@click.argument("bar_or_none", metavar="BAR", required=False)
def main(*, bar_or_foo: str, bar_or_none: bar | None) -> None:
if bar_or_none is None:
foo = None
bar = bar_or_foo
else:
foo = bar_or_foo
bar = bar_or_none
click.echo(f"foo={foo!r} bar={bar!r}")
if __name__ == "__main__": main()
but this is fiddly and error-prone.
Right now, I only want this for programs which take exactly one or two command line arguments, of which the first is optional and the second is required. A logical generalization would be to programs that take one or more optional command line arguments, followed by one or more required command line arguments. The parser should count the actual number of non-option arguments supplied, issue an error if the count is less than the number of required arguments, fill in the required arguments left to right with the last N non-option arguments, and then fill in the optional arguments left to right with whatever's left over.
Further generalization - to an arbitrary intermingling of required and optional - strikes me as undesirable, as the resultant CLI would be very confusing. However, Click should error out if the programmer tries to specify a mingled order, rather than generating a --help text that looks like nothing is wrong.
I am extremely hesitant to add any more complexity to how arguments are matched. While investigating rewriting the parser, you can see how complicated it already is: https://github.com/pallets/click/compare/main...parser-rewrite-1#diff-11ba83cac151f7b24a1ed7c31a2a522d24d190cfa43199aa478d1e9cd2e6c610R1384-R1427 It gets really complicate to explain or predict, even your first generalization is really complicated to write out.
I'll keep it in mind, since I'm rewriting the parser anyway. But this is unlikely to happen any time soon.
would confuse people into thinking that the directory is being checked against the label
Would it? I wouldn't be confused by that. If users could be confused, it's all down to documentation.
Click should error out if the programmer tries to specify a mingled order
We are unlikely to do this. The more processing and checks we do at setup, the slower the command feels to the user. For a CLI, we need to balance dev checks with reasonable speed, usually in favor of speed.
I appreciate a desire not to make the parser any more complicated; however, since this does already work for nargs=-1, it seems to me that it shouldn't be that much harder for a zero-or-one argument.
Looking at that code, is there a reason why the argument tokens are being processed in reverse order? It seems to add quite a bit of complexity.
would confuse people into thinking that the directory is being checked against the label
Would it? I wouldn't be confused by that. If users could be confused, it's all down to documentation.
I haven't actually gone out and done a user study on this, but I think you and I are used to command line tools taking their arguments in wacky orders. This thing I'm working on is for people who aren't professional software people. Their threshold of confusion is a lot lower, and they tend to get annoyed with people who tell them to read the documentation.
we need to balance dev checks with reasonable speed
Also understandable. Maybe a sanity check mode that applications could run from their unit tests, instead?