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