命令和组

click最重要的特性是任意嵌套命令行实用程序的概念。这是通过 CommandGroup (实际上) MultiCommand

回调调用

对于常规命令,每当命令运行时都会执行回调。如果脚本是唯一的命令,它将始终启动(除非参数回调阻止它)。例如,如果有人通过 --help 剧本)

对于组和多个命令,情况看起来不同。在这种情况下,每当子命令触发时,回调都会触发(除非更改了此行为)。实际上,这意味着当内部命令运行时,外部命令将运行:

@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
    click.echo(f"Debug mode is {'on' if debug else 'off'}")

@cli.command()  # @cli, not @click!
def sync():
    click.echo('Syncing')

这里是这样的:

$ tool.py
Usage: tool.py [OPTIONS] COMMAND [ARGS]...

Options:
  --debug / --no-debug
  --help                Show this message and exit.

Commands:
  sync

$ tool.py --debug sync
Debug mode is on
Syncing

传递参数

Click“严格分隔命令和子命令之间的参数”。这意味着必须指定特定命令的选项和参数 之后 命令名本身,但是 之前 任何其他命令名。

这种行为已经可以通过预先定义的 --help 选择权。假设我们有一个叫做 tool.py ,包含名为 sub .

  • tool.py --help 将返回整个程序的帮助(列出子命令)。

  • tool.py sub --help 将返回的帮助 sub 子命令。

  • 但是 tool.py --help sub 会治疗 --help 作为主程序的参数。Click然后调用回调 --help ,这将打印帮助并在Click“可以处理子命令”之前中止程序。

嵌套处理和上下文

从前面的示例中可以看到,基本命令组接受一个调试参数,该参数将传递给它的回调,但不会传递给同步命令本身。sync命令只接受自己的参数。

这使得工具可以完全独立地执行操作,但是一个命令如何与嵌套的命令进行通信?答案是 Context .

每次调用命令时,都会创建一个新的上下文并与父上下文链接。通常,你看不到这些上下文,但它们就在那里。上下文与值一起自动传递给参数回调。命令还可以通过使用 pass_context() 装饰者。在这种情况下,上下文作为第一个参数传递。

上下文还可以携带可用于程序目的的程序指定对象。这意味着您可以构建这样的脚本:

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    # ensure that ctx.obj exists and is a dict (in case `cli()` is called
    # by means other than the `if` block below)
    ctx.ensure_object(dict)

    ctx.obj['DEBUG'] = debug

@cli.command()
@click.pass_context
def sync(ctx):
    click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")

if __name__ == '__main__':
    cli(obj={})

如果提供了对象,则每个上下文都会将对象传递给其子级,但在任何级别上,都可以覆盖上下文的对象。要联系父母, context.parent 可以使用。

除此之外,没有什么可以阻止应用程序修改全局状态,而不是向下传递对象。例如,您可以翻转一个全局 DEBUG 变量并用它来完成。

修饰命令

正如您在前面的示例中看到的,装饰器可以更改调用命令的方式。在后台实际发生的情况是,回调总是通过 Context.invoke() 方法自动正确调用命令(通过传递上下文或不传递上下文)。

当您想编写自定义装饰器时,这非常有用。例如,一个常见的模式是配置一个表示状态的对象,然后将其存储在上下文中,然后使用一个自定义装饰器来查找这种类型的最新对象,并将其作为第一个参数传递。

例如, pass_obj() decorator可以这样实现:

from functools import update_wrapper

def pass_obj(f):
    @click.pass_context
    def new_func(ctx, *args, **kwargs):
        return ctx.invoke(f, ctx.obj, *args, **kwargs)
    return update_wrapper(new_func, f)

这个 Context.invoke() 命令将以正确的方式自动调用函数,因此将使用 f(ctx, obj)f(obj) 取决于它本身是否用 pass_context() .

这是一个非常强大的概念,可用于构建非常复杂的嵌套应用程序;请参见 复杂的应用 更多信息。

不带命令的组调用

默认情况下,除非传递子命令,否则不会调用group或multi命令。实际上,不提供命令会自动传递 --help 默认情况下。此行为可以通过传递来更改 invoke_without_command=True 一组。在这种情况下,总是调用回调,而不是显示帮助页面。Context对象还包含有关调用是否将转到子命令的信息。

例子:

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        click.echo('I was invoked without subcommand')
    else:
        click.echo(f"I am about to invoke {ctx.invoked_subcommand}")

@cli.command()
def sync():
    click.echo('The subcommand')

以及它在实践中的工作方式:

$ tool
I was invoked without subcommand
$ tool sync
I am about to invoke sync
The subcommand

自定义多命令

除了使用 click.group() ,您还可以构建自己的自定义多命令。当您希望支持从插件中延迟加载的命令时,这很有用。

自定义多命令只需要实现一个列表和加载方法:

import click
import os

plugin_folder = os.path.join(os.path.dirname(__file__), 'commands')

class MyCLI(click.MultiCommand):

    def list_commands(self, ctx):
        rv = []
        for filename in os.listdir(plugin_folder):
            if filename.endswith('.py') and filename != '__init__.py':
                rv.append(filename[:-3])
        rv.sort()
        return rv

    def get_command(self, ctx, name):
        ns = {}
        fn = os.path.join(plugin_folder, name + '.py')
        with open(fn) as f:
            code = compile(f.read(), fn, 'exec')
            eval(code, ns, ns)
        return ns['cli']

cli = MyCLI(help='This tool\'s subcommands are loaded from a '
            'plugin folder dynamically.')

if __name__ == '__main__':
    cli()

这些自定义类也可以与装饰器一起使用:

@click.command(cls=MyCLI)
def cli():
    pass

合并多个命令

除了实现自定义多命令之外,将多个命令合并到一个脚本中也很有趣。虽然这通常不如建议的那样,因为它将一个嵌套在另一个之下,但在某些情况下,合并方法对于更好的shell体验是有用的。

这种合并系统的默认实现是 CommandCollection 类。它接受其他多命令的列表,并使这些命令在同一级别上可用。

示例用法:

import click

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

@cli1.command()
def cmd1():
    """Command on cli1"""

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

@cli2.command()
def cmd2():
    """Command on cli2"""

cli = click.CommandCollection(sources=[cli1, cli2])

if __name__ == '__main__':
    cli()

看起来像是:

$ cli --help
Usage: cli [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  cmd1  Command on cli1
  cmd2  Command on cli2

如果一个命令存在于多个源中,则第一个源获胜。

多命令链接

Changelog

3.0 新版功能.

有时允许一次调用多个子命令很有用。例如,如果您在熟悉 setup.py sdist bdist_wheel upload 调用的命令链 sdist 之前 bdist_wheel 之前 upload . 从Click3.0开始,这是非常简单的实现。你所要做的就是通过 chain=True 到您的多命令:

@click.group(chain=True)
def cli():
    pass


@cli.command('sdist')
def sdist():
    click.echo('sdist called')


@cli.command('bdist_wheel')
def bdist_wheel():
    click.echo('bdist_wheel called')

现在您可以这样调用它:

$ setup.py sdist bdist_wheel
sdist called
bdist_wheel called

使用多命令链接时,只能使用一个命令(最后一个) nargs=-1 在争论中。也不可能将多个命令嵌套在链式多命令之下。除此之外,他们的工作方式没有任何限制。他们可以像平常一样接受选项和参数。对于链式命令,选项和参数之间的顺序是有限的。仅限当前 --options argument 允许订购。

另一个注意事项: Context.invoked_subcommand 属性对于多个命令来说有点无用,因为它会 '*' 如果调用了多个命令,则作为值。这是必要的,因为子命令的处理是一个接一个的,所以当回调触发时,将要处理的确切子命令尚不可用。

注解

链命令当前无法嵌套。这将在以后的版本中修复。

多命令管道

Changelog

3.0 新版功能.

多命令链接的一个非常常见的用例是让一个命令处理上一个命令的结果。有多种方法可以促进这一点。最明显的方法是在上下文对象上存储一个值,并将其从一个函数处理到另一个函数。这是通过用 pass_context() 之后,将提供上下文对象,并且子命令可以在其中存储其数据。

另一种方法是通过返回处理函数来设置管道。这样想:当一个子命令被调用时,它会处理它的所有参数,并提出一个如何处理它的计划。然后它返回一个处理函数并返回。

返回的函数去哪里?链接的多命令可以用 MultiCommand.resultcallback() 它遍历所有这些函数,然后调用它们。

要使其更具体一点,请考虑以下示例:

@click.group(chain=True, invoke_without_command=True)
@click.option('-i', '--input', type=click.File('r'))
def cli(input):
    pass

@cli.resultcallback()
def process_pipeline(processors, input):
    iterator = (x.rstrip('\r\n') for x in input)
    for processor in processors:
        iterator = processor(iterator)
    for item in iterator:
        click.echo(item)

@cli.command('uppercase')
def make_uppercase():
    def processor(iterator):
        for line in iterator:
            yield line.upper()
    return processor

@cli.command('lowercase')
def make_lowercase():
    def processor(iterator):
        for line in iterator:
            yield line.lower()
    return processor

@cli.command('strip')
def make_strip():
    def processor(iterator):
        for line in iterator:
            yield line.strip()
    return processor

这是一步一步完成的,所以我们一步一步地完成它。

  1. 首先要做的是 group() 那是可以锁链的。此外,我们还指示Click以调用,即使没有定义子命令。如果不这样做,那么调用空管道将生成帮助页,而不是运行结果回调。

  2. 接下来我们要做的就是在我们的组中注册一个结果回调。此回调将使用一个参数调用,该参数是所有子命令的所有返回值的列表,然后是与组本身相同的关键字参数。这意味着我们可以轻松地访问输入文件,而不必使用上下文对象。

  3. 在这个结果回调中,我们创建一个输入文件中所有行的迭代器,然后通过所有子命令返回的所有回调传递这个迭代器,最后我们将所有行打印到stdout。

在这之后,我们可以注册尽可能多的子命令,并且每个子命令都可以返回一个处理器函数来修改行流。

需要注意的一点是,每次运行回调之后,Click都会关闭上下文。这意味着,例如,在 processor 因为文件将在那里关闭。这种限制不太可能改变,因为它会使资源处理更加复杂。为此,建议不要使用文件类型并通过手动打开文件 open_file() .

对于一个更复杂的例子,在处理管道时也有所改进,请看 imagepipe multi command chaining demo 在Click存储库中。它实现了一个基于管道的图像编辑工具,该工具具有良好的管道内部结构。

覆盖默认值

默认情况下,参数的默认值从 default 定义默认值时提供的标志,但这不是唯一可以从中加载默认值的位置。另一个地方是 Context.default_map (词典)上下文。这允许从配置文件加载默认值以覆盖常规默认值。

如果您从另一个包中插入了一些命令,但对默认值不满意,那么这将非常有用。

可以为每个子命令任意嵌套默认映射:

default_map = {
    "debug": True,  # default for a top level option
    "runserver": {"port": 5000}  # default for a subcommand
}

当调用脚本时,可以提供默认映射,或者在任何时候由命令重写。例如,顶级命令可以从配置文件加载默认值。

示例用法:

import click

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

@cli.command()
@click.option('--port', default=8000)
def runserver(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")

if __name__ == '__main__':
    cli(default_map={
        'runserver': {
            'port': 5000
        }
    })

行动中:

$ cli runserver
Serving on http://127.0.0.1:5000/

上下文默认值

Changelog

2.0 新版功能.

从click 2.0开始,您不仅可以在调用脚本时,还可以在声明命令的装饰器中覆盖上下文的默认值。例如,前面的示例定义了一个自定义 default_map 这也可以在装饰器中完成。

此示例与前一个示例相同:

import click

CONTEXT_SETTINGS = dict(
    default_map={'runserver': {'port': 5000}}
)

@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
    pass

@cli.command()
@click.option('--port', default=8000)
def runserver(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")

if __name__ == '__main__':
    cli()

同样的例子是:

$ cli runserver
Serving on http://127.0.0.1:5000/

命令返回值

Changelog

3.0 新版功能.

click 3.0中的一个新介绍是完全支持命令回调的返回值。这使得以前难以实现的一系列功能得以实现。

实际上,任何命令回调现在都可以返回一个值。此返回值冒泡到某些接收器。其中一个用例已经在 多命令链接 其中已经证明,链接的多个命令可以具有处理所有返回值的回调。

在click中使用命令返回值时,您需要知道:

  • 命令回调的返回值通常从 BaseCommand.invoke() 方法。这条规则的例外与 Group S:

    • 在组中,返回值通常是所调用子命令的返回值。此规则的唯一例外是,如果在没有参数和 invoke_without_command 启用。

    • 如果为链接设置了组,则返回值是所有子命令结果的列表。

    • 组的返回值可以通过 MultiCommand.result_callback . 这是通过链模式下所有返回值的列表调用的,或者在非链命令的情况下使用单个返回值调用的。

  • 返回值从 Context.invoke()Context.forward() 方法。这在内部希望调用另一个命令的情况下很有用。

  • click对返回值没有任何硬要求,也不使用它们本身。这允许将返回值用于自定义装饰器或工作流(如在多命令链接示例中)。

  • 当Click脚本作为命令行应用程序调用时(通过 BaseCommand.main() )返回值将被忽略,除非 standalone_mode 在这种情况下它是无效的。