Is there an equivalent to argparse
's nargs='*'
functionality for optional arguments in Click?
I am writing a command line script, and one of the options needs to be able to take an unlimited number of arguments, like:
foo --users alice bob charlie --bar baz
So users
would be ['alice', 'bob', 'charlie']
and bar
would be 'baz'
.
In argparse
, I can specify multiple optional arguments to collect all of the arguments that follow them by setting nargs='*'
.
>>> parser = argparse.ArgumentParser() >>> parser.add_argument('--users', nargs='*') >>> parser.add_argument('--bar') >>> parser.parse_args('--users alice bob charlie --bar baz'.split()) Namespace(bar='baz', users=['alice', 'bob', 'charlie'])
I know Click allows you to specify an argument to accept unlimited inputs by setting nargs=-1
, but when I try to set an optional argument's nargs
to -1, I get:
TypeError: Options cannot have nargs < 0
Is there a way to make Click accept an unspecified number of arguments for an option?
I need to be able to specify options after the option that takes unlimited arguments.
@Stephen Rauch's answer answers this question. However, I don't recommend using the approach I ask for here. My feature request is intentionally not implemented in Click, since it can result in unexpected behaviors. Click's recommended approach is to use multiple=True
:
@click.option('-u', '--user', 'users', multiple=True)
And in the command line, it will look like:
foo -u alice -u bob -u charlie --bar baz
Click, or “Command Line Interface Creation Kit” is a Python library for building command line interfaces. The three main points of Python Click are arbitrary nesting of commands, automatic help page generation, and supporting lazy loading of subcommands at runtime.
Click supports two types of parameters for scripts: options and arguments. There is generally some confusion among authors of command line scripts of when to use which, so here is a quick overview of the differences. As its name indicates, an option is optional.
The Python argparse library was released as part of the standard library with Python 3.2 on February the 20th, 2011. It was introduced with Python Enhancement Proposal 389 and is now the standard way to create a CLI in Python, both in 2.7 and 3.2+ versions.
One way to approach what you are after is to inherit from click.Option, and customize the parser.
import click class OptionEatAll(click.Option): def __init__(self, *args, **kwargs): self.save_other_options = kwargs.pop('save_other_options', True) nargs = kwargs.pop('nargs', -1) assert nargs == -1, 'nargs, if set, must be -1 not {}'.format(nargs) super(OptionEatAll, self).__init__(*args, **kwargs) self._previous_parser_process = None self._eat_all_parser = None def add_to_parser(self, parser, ctx): def parser_process(value, state): # method to hook to the parser.process done = False value = [value] if self.save_other_options: # grab everything up to the next option while state.rargs and not done: for prefix in self._eat_all_parser.prefixes: if state.rargs[0].startswith(prefix): done = True if not done: value.append(state.rargs.pop(0)) else: # grab everything remaining value += state.rargs state.rargs[:] = [] value = tuple(value) # call the actual process self._previous_parser_process(value, state) retval = super(OptionEatAll, self).add_to_parser(parser, ctx) for name in self.opts: our_parser = parser._long_opt.get(name) or parser._short_opt.get(name) if our_parser: self._eat_all_parser = our_parser self._previous_parser_process = our_parser.process our_parser.process = parser_process break return retval
To use the custom class, pass the cls
parameter to @click.option()
decorator like:
@click.option("--an_option", cls=OptionEatAll)
or if it is desired that the option will eat the entire rest of the command line, not respecting other options:
@click.option("--an_option", cls=OptionEatAll, save_other_options=False)
This works because click is a well designed OO framework. The @click.option()
decorator usually instantiates a click.Option
object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option
in our own class and over ride the desired methods.
In this case we over ride click.Option.add_to_parser()
and the monkey patch the parser so that we can eat more than one token if desired.
@click.command() @click.option('-g', 'greedy', cls=OptionEatAll, save_other_options=False) @click.option('--polite', cls=OptionEatAll) @click.option('--other') def foo(polite, greedy, other): click.echo('greedy: {}'.format(greedy)) click.echo('polite: {}'.format(polite)) click.echo('other: {}'.format(other)) if __name__ == "__main__": commands = ( '-g a b --polite x', '-g a --polite x y --other o', '--polite x y --other o', '--polite x -g a b c --other o', '--polite x --other o -g a b c', '-g a b c', '-g a', '-g', 'extra', '--help', ) import sys, time time.sleep(1) print('Click Version: {}'.format(click.__version__)) print('Python Version: {}'.format(sys.version)) for cmd in commands: try: time.sleep(0.1) print('-----------') print('> ' + cmd) time.sleep(0.1) foo(cmd.split()) except BaseException as exc: if str(exc) != '0' and \ not isinstance(exc, (click.ClickException, SystemExit)): raise
Click Version: 6.7 Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] ----------- > -g a b --polite x greedy: ('a', 'b', '--polite', 'x') polite: None other: None ----------- > -g a --polite x y --other o greedy: ('a', '--polite', 'x', 'y', '--other', 'o') polite: None other: None ----------- > --polite x y --other o greedy: None polite: ('x', 'y') other: o ----------- > --polite x -g a b c --other o greedy: ('a', 'b', 'c', '--other', 'o') polite: ('x',) other: None ----------- > --polite x --other o -g a b c greedy: ('a', 'b', 'c') polite: ('x',) other: o ----------- > -g a b c greedy: ('a', 'b', 'c') polite: None other: None ----------- > -g a greedy: ('a',) polite: None other: None ----------- > -g Error: -g option requires an argument ----------- > extra Usage: test.py [OPTIONS] Error: Got unexpected extra argument (extra) ----------- > --help Usage: test.py [OPTIONS] Options: -g TEXT --polite TEXT --other TEXT --help Show this message and exit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With