18

I'm using the Python Click library for my command-line interface. I'd like to have a command that takes multiple key value pairs. I'm flexible on the api. For example

my_cli my_command FOO=1 BAR=2

or maybe

my_cli my_command FOO 1 BAR 2

or even

my_cli my_command {"FOO": 1, "BAR": 2}

Is there an easy way to do this with Click?

3
  • Is it acceptable to just ask click to give you unlimited arguments, then my_command will get them all as a single tuple (like *args in regular Python) and iterate over them and pair them up? Commented Jul 3, 2018 at 23:51
  • If it is acceptable, see my answer. If it isn't—e.g., you need to take advantage of the built-in way Click handles options in a way that can accept --foo=1, --foo:1, --foo 1, but need arbitrary option-like-things and without -- prefixes—then I think it's impossible. You could write a new Parameter subclass that handles these, but that's explicitly not supported, and may be broken by even minor updates to click. Commented Jul 4, 2018 at 0:05
  • One last thing: You might want to consider submitting a feature request (if there isn't one there already). It might help to refer to it as, say, "dd-style operands", and come up with other common examples that are equivalent to what you want, both to make your request seem more useful/normal, and to provide a good test case if someone does decide to implement it. Commented Jul 4, 2018 at 0:09

4 Answers 4

13

The same is possible with options instead of arguments, see Tuples as Multi Value Options in combination with Multiple Options.

import click

@click.command()
@click.option("--dict", "-d", "mydict", type=(str, int), multiple=True)
def cli(mydict):
    d = dict(mydict)
    click.echo(d)

if __name__ == "__main__":
    cli()

Example:

$ python3 ./clickexample.py -d hello 1 -d foo 2 -d baz 3
{'hello': 1, 'foo': 2, 'baz': 3}
Sign up to request clarification or add additional context in comments.

3 Comments

the good solution! but sadly -d foo=2 dont work
hmm i'll need to look at this, but maybe you can put the dict(mydict) operation into a callback ?
@ThomasDecaux well the treatment would have to be different since "foo=2" would be treated by the shell as a single string
5

The simplest solution is basically the same thing you'd do with a regular Python function where you wanted an API like this.

Take a single parameter that groups the variable-length stream of arguments into a tuple. Then, what you do depends on whether you want separate arguments:

>>> def func(*args):
...     d = dict(zip(args[::2], args[1::2]))
...     print(d)
>>> func('FOO', 1, 'BAR', 2)
{'FOO': 1, 'BAR': 2}

… or combined key:value arguments:

>>> def func(*args):
...     d = dict(arg.split(':') for arg in args)
...     print(d)

This one is a bit hacky to use, because in Python, arguments aren't just space-separated words, but bear with me on that:

>>> func('FOO:1', 'BAR:2')
{'FOO': 1, 'BAR': 2}

The click equivalent for the first looks like this:

@click.command()
@click.argument('args', nargs=-1)
def my_command(args):
    d = dict(zip(args[::2], args[1::2]))
    click.echo(d)

(Obviously you can stick that in a click.group, etc., just like any other command.)

And now:

$ ./clicky.py FOO 1 BAR 2
{'FOO': 1, 'BAR': 2}

And the second looks like this:

@click.command()
@click.argument('args', nargs=-1)
def my_command(args):
    d = dict(arg.split(':') for arg in args)
    click.echo(d)

And notice that now, using it is not hacky at all, because to your shell, arguments are just words separated by spaces:

$ ./clicky.py FOO:1 BAR:2
{'FOO': 1, 'BAR': 2}

What if you want to handle both KEY=VALUE and KEY:VALUE? Then you just have to write something slightly more complicated than arg.split(':'). And you'll probably want some better error handling too. But that should be enough to get you started.

Comments

2

You can do this in click using a callback like so:


def _attributes_to_dict(
    ctx: click.Context, attribute: click.Option, attributes: tuple[str, ...]
) -> dict[str, str]:
    """Click callback that converts attributes specified in the form `key=value` to a
    dictionary"""
    result = {}
    for arg in attributes:
        k, v = arg.split("=")
        if k in result:
            raise click.BadParameter(f"Attribute {k!r} is specified twice")
        result[k] = v

    return result


@click.command()
@click.option(
    "-a",
    "--attributes",
    help="Attributes in the form key=value. Can be specified multiple times.",
    multiple=True,
    callback=_attributes_to_dict,
)
def cli(attributes: dict[str, str]):
    print(attributes)


if __name__ == "__main__":
    cli()

Usage:

>>> python cli.py -a key=value -a other_key=other_value
{'key': 'value', 'other_key': 'other_value'}

Comments

0

I used this custom click type because I needed a lot of custom options, including list, int, str.

... command --param='page=1; name=Items; rules=1, 2, three; extra=A,;'
{'page': 1, 'name': 'Items', 'rules': [1, 2, 'three'], 'extra': ['A']}

Usage:

>>> @click.option("--param", default=None, type=DictParamType())
... def command(param):
...     ...

Example:

>>> param_value = 'page=1; name=Items; rules=1, 2, three; extra=A,;'
>>> DictParamType().convert(param_value, None, None)
{'page': 1, 'name': 'Items', 'rules': [1, 2, 'three'], 'extra': ['A']}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.