How to add common options to sub commands which can go *after* the name of the sub command
Asked Answered
A

2

16

Using the CLI library click I have an application script app.py with the two sub commands read and write:

@click.group()
@click.pass_context
def cli(ctx):
    pass

@cli.command()
@click.pass_context
def read(ctx):
    print("read")

@cli.command()
@click.pass_context
def write(ctx):
    print("write")

I want to declare a common option --format. I know I can add it as an option to the command group via

@click.group()
@click.option('--format', default='json')
@click.pass_context
def cli(ctx, format):
    ctx.obj['format'] = format

But then I cannot give the option after the command, which in my use case is a lot more natural. I want to be able to issue in the shell:

app.py read --format XXX 

But with the outlined set-up I get the message Error: no such option: --format. The script only accepts the option before the command.

So my question is: How can I add a common option to both sub commands so that it works as if the option were given to each sub command?

Acaleph answered 3/9, 2018 at 6:41 Comment(2)
What do you mean with cannot give the option after the subcommand? If you put the format on cli, you execute it with read --format XXX right?Biotype
@ThePjot No, that is exactly the problem: When I issue on the command line app.py read --format XXX I get the error Error: no such option: --format. Only app.py --format XXX read works. (I will update the question to make it clearer.)Acaleph
L
15

AFAICT, this is not possible with Click. The docs state that:

Click strictly separates parameters between commands and subcommands. What this means is that options and arguments for a specific command have to be specified after the command name itself, but before any other command names.

A possible workaround is writing a common_options decorator. The following example is using the fact that click.option is a function that returns a decorator function which expects to be applied in series. IOW, the following:

@click.option("-a")
@click.option("-b")
def hello(a, b):
    pass

is equivalent to the following:

def hello(a, b):
    pass

hello = click.option("-a")(click.option("-b")(hello))

The drawback is that you need to have the common argument set on all your subcommands. This can be resolved through **kwargs, which collects keyword arguments as a dict.

(Alternately, you could write a more advanced decorator that would feed the arguments into the context or something like that, but my simple attempt didn't work and i'm not ready to try more advanced approaches. I might edit the answer later and add them.)

With that, we can make a program:

import click
import functools

@click.group()
def cli():
    pass

def common_options(f):
    options = [
        click.option("-a", is_flag=True),
        click.option("-b", is_flag=True),
    ]
    return functools.reduce(lambda x, opt: opt(x), options, f)

@cli.command()
@common_options
def hello(**kwargs):
    print(kwargs)
    # to get the value of b:
    print(kwargs["b"])

@cli.command()
@common_options
@click.option("-c", "--citrus")
def world(citrus, a, **kwargs):
    print("citrus is", citrus)
    if a:
        print(kwargs)
    else:
        print("a was not passed")

if __name__ == "__main__":
    cli()
Lei answered 3/9, 2018 at 9:51 Comment(1)
+1 good answer! The maintainers confirm they won't be adding this, but there are similar workarounds in the GitHub issue: github.com/pallets/click/issues/108#issuecomment-194465429Mass
I
1

Yes you can add common options to sub commands which can go after the name of the sub command.
You can have options on parent command as well as on sub commands.

Check out below code snippet

import click
from functools import wraps

@click.group()
def cli():
    pass

def common_options(f):
    @wraps(f)
    @click.option('--option1', '-op1', help='Option 1 help text', type=click.FLOAT)
    @click.option('--option2', '-op2', help='Option 2 help text', type=click.FLOAT)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)

    return wrapper

@cli.group(invoke_without_command=True)
@common_options
@click.pass_context
def parent(ctx, option1, option2):
    ctx.ensure_object(dict)
    if ctx.invoked_subcommand is None:
         click.secho('Parent group is invoked. Perform specific tasks to do!', fg='bright_green')

@parent.command()
@click.option('--sub_option1', '-sop1', help='Sub option 1 help text', type=click.FLOAT)
@common_options
def sub_command1(option1, option2, sub_option1):
    click.secho('Perform sub command 1 operations', fg='bright_green')

@parent.command()
@click.option('--sub_option2', '-sop2', help='Sub option 2 help text', type=click.FLOAT)
@common_options
def sub_command2(option1, option2, sub_option2):
    click.secho('Perform sub command 2 operations', fg='bright_green')

if __name__ == "__main__":
    cli()

Usage

parent --help
=> prints parent group help text with options and sub commands

parent --option1 10 --option2 12
=> Parent group is invoked. Perform specific tasks to do!

parent sub_command1 --help
=> prints sub command 1 help text with options on sub commands

parent sub_command1 --option1 15 --option2 7 --sub_option1 5
=> Perform sub command 1 operations

parent sub_command2 --option1 15 --option2 7 --sub_option2 4
=> Perform sub command 2 operations
Inhesion answered 10/5, 2022 at 13:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.