如何向子命令添加通用选项,这些选项可以在子命令的名称 * 之后 *

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

使用 CLI 库 click 我有一个应用程序脚本 app.py 带有两个子命令 readwrite:

@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")

我想声明一个通用选项--format。我知道我可以将它作为选项添加到命令 group via

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

但是我不能在命令之后给出选项,这在我的用例中更自然。我希望能够在 shell:

中发布
app.py read --format XXX 

但是通过概述的设置,我收到消息 Error: no such option: --format。该脚本只接受选项 before 命令。

所以我的问题是:如何为两个子命令添加一个公共选项,以便它像为每个子命令提供该选项一样工作?

AFAICT,Click 无法做到这一点。文档指出:

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.

一个可能的解决方法是编写一个 common_options 装饰器。下面的示例使用了 click.option 是一个函数,而 returns 是一个装饰函数,它期望被串联应用。 IOW,以下内容:

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

等同于:

def hello(a, b):
    pass

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

缺点是您需要在所有子命令上设置通用参数。这可以通过 **kwargs 解决,它将关键字参数收集为字典。

(或者,您可以编写一个更高级的装饰器,将参数输入上下文或类似的东西,但我的简单尝试没有奏效,我还没有准备好尝试更高级的方法。我可能稍后编辑答案并添加它们。)

有了它,我们可以制作一个程序:

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()

是的,您可以向子命令添加常用选项,这些选项可以跟在子命令的名称之后。
您可以在父命令和子命令上有选项。

查看下面的代码片段

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()

用法

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