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?
subcommand (plural subcommands) (computing) A command that makes up part of a larger command. This command accepts additional subcommands as parameters.
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.
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.
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.
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With