How to accept an indefinite number of options (and

2019-08-14 05:41发布

问题:

I want to pass an unlimited number of options to a click CLI. I don't know Option names either. I'm getting around this issue by using an option named conf. It accepts a string that is assumed to represent a JSON object.

What I've done:

@click.command()
@click.option('--conf', type=str)
def dummy(conf):
    click.echo('dummy param {}'.format(conf))

How I use it:

>python main.py dummy --conf='{"foo": "bar", "fizz": "buzz"}'

What I want to do:

@click.command()
#some magic stuff
def dummy(**kwargs):
    click.echo('dummy param {}'.format(**kwargs))

How I want to use it:

>python main.py dummy --foo=bar --fizz=buzz

回答1:

You can hook the parser and make it aware of each option given from the command line like:

Custom Command Class:

import click

class AcceptAllCommand(click.Command):

    def make_parser(self, ctx):
        """Hook 'make_parser' and allow the opt dict to find any option"""
        parser = super(AcceptAllCommand, self).make_parser(ctx)
        command = self

        class AcceptAllDict(dict):

            def __contains__(self, item):
                """If the parser does no know this option, add it"""

                if not super(AcceptAllDict, self).__contains__(item):
                    # create an option name
                    name = item.lstrip('-')

                    # add the option to our command
                    click.option(item)(command)

                    # get the option instance from the command
                    option = command.params[-1]

                    # add the option instance to the parser
                    parser.add_option(
                        [item], name.replace('-', '_'), obj=option)
                return True

        # set the parser options to our dict
        parser._short_opt = AcceptAllDict(parser._short_opt)
        parser._long_opt = AcceptAllDict(parser._long_opt)

        return parser

Using the Custom Class:

To use the custom class, just pass the class to the click.command() decorator like:

@click.command(cls=AcceptAllCommand)
def my_command(**kwargs):
    ...

How does this work?

This works because click is a well designed OO framework. The @click.command() decorator usually instantiates a click.Command object but allows this behavior to be over-ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command in our own class and over ride the desired methods.

In this case, we override make_parser() and replace the option dicts with a dict class of our own. In our dict we override the __contains__() magic method, and in it we populate the parser with an option matching the name that is being looked for, if that name does not already exist.

Test Code:

@click.command(cls=AcceptAllCommand)
def dummy(**kwargs):
    click.echo('dummy param: {}'.format(kwargs))

if __name__ == "__main__":
    import time
    cmd = '--foo=bar --fizz=buzz'

    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    print('-----------')
    print('> ' + cmd)
    time.sleep(0.1)
    dummy(cmd.split())

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)]
-----------
> --foo=bar --fizz=buzz
dummy param: {'foo': 'bar', 'fizz': 'buzz'}