Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

nargs=* equivalent for options in Click

Tags:

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?

Update:

I need to be able to specify options after the option that takes unlimited arguments.

Update:

@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 
like image 911
jpyams Avatar asked Jan 22 '18 23:01

jpyams


People also ask

What is Python click option?

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.

Which of the following is parameter of the click command?

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.

Is argparse included in Python?

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.


1 Answers

One way to approach what you are after is to inherit from click.Option, and customize the parser.

Custom Class:

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 

Using Custom Class:

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) 

How does this work?

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.

Test Code:

@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 

Test Results:

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. 
like image 52
Stephen Rauch Avatar answered Sep 18 '22 21:09

Stephen Rauch