Search code examples
pythoncommand-lineparameterspython-click

How to pass several list of arguments to @click.option


I want to call a python script through the command line with this kind of parameter (list could be any size, eg with 3):

python test.py --option1 ["o11", "o12", "o13"] --option2 ["o21", "o22", "o23"]

using click. From the docs, it is not stated anywhere that we can use a list as parameter to @click.option

And when I try to do this:

#!/usr/bin/env python
import click

@click.command(context_settings=dict(help_option_names=['-h', '--help']))
@click.option('--option', default=[])
def do_stuff(option):

    return

# do stuff
if __name__ == '__main__':
    do_stuff()

in my test.py, by calling it from the command line:

python test.py --option ["some option", "some option 2"]

I get an error:

Error: Got unexpected extra argument (some option 2])

I can't really use variadic arguments as only 1 variadic arguments per command is allowed (http://click.pocoo.org/5/arguments/#variadic-arguments)

So if anyone can point me to the right direction (using click preferably) it would be very much appreciated.


Solution

  • You can coerce click into taking multiple list arguments, if the lists are formatted as a string literals of python lists by using a custom option class like:

    Custom Class:

    import click
    import ast
    
    class PythonLiteralOption(click.Option):
    
        def type_cast_value(self, ctx, value):
            try:
                return ast.literal_eval(value)
            except:
                raise click.BadParameter(value)
    

    This class will use Python's Abstract Syntax Tree module to parse the parameter as a python literal.

    Custom Class Usage:

    To use the custom class, pass the cls parameter to @click.option() decorator like:

    @click.option('--option1', cls=PythonLiteralOption, default=[])
    

    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.type_cast_value() and then call ast.literal_eval() to parse the list.

    Test Code:

    @click.command(context_settings=dict(help_option_names=['-h', '--help']))
    @click.option('--option1', cls=PythonLiteralOption, default=[])
    @click.option('--option2', cls=PythonLiteralOption, default=[])
    def cli(option1, option2):
        click.echo("Option 1, type: {}  value: {}".format(
            type(option1), option1))
        click.echo("Option 2, type: {}  value: {}".format(
            type(option2), option2))
    
    # do stuff
    if __name__ == '__main__':
        import shlex
        cli(shlex.split(
            '''--option1 '["o11", "o12", "o13"]' 
            --option2 '["o21", "o22", "o23"]' '''))
    

    Test Results:

    Option 1, type: <type 'list'>  value: ['o11', 'o12', 'o13']
    Option 2, type: <type 'list'>  value: ['o21', 'o22', 'o23']