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

downstroy picture downstroy · Dec 4, 2017 · Viewed 25.9k times · Source

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.

Answer

Stephen Rauch picture Stephen Rauch · Dec 9, 2017

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']