Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 a 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?

like image 688
halloleo Avatar asked Sep 03 '18 06:09

halloleo


People also ask

What is the sub command?

subcommand (plural subcommands) (computing) A command that makes up part of a larger command. This command accepts additional subcommands as parameters.

How do I use the command line option?

In MS-DOS/Windows, by convention, a command-line option is indicated by a letter prefixed with a forward slash. As an example, the XCOPY command, which is used for copying files and directories, can be started using the the following options, among others: /T -- copy the directory structure only.

What is a subcommand CLI?

Subcommands are keyword that invoke a new set of options and features. For example, the git command has a long series of subcommands, like add and commit . Each can have its own options and implementations.

What is click Pass_context?

This is a special attribute where commands are supposed to remember what they need to pass on to their children. In order for this to work, we need to mark our function with pass_context() , because otherwise, the context object would be entirely hidden from us.


1 Answers

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()
like image 172
matejcik Avatar answered Sep 28 '22 05:09

matejcik