高级模式

除了在库中实现的公共功能之外,还有无数的模式可以通过扩展单击来实现。这一页应该提供一些可以完成的细节。

命令别名

许多工具支持命令的别名。例如,您可以配置 git 接受 git ci 作为的别名 git commit 。其他工具也通过自动缩短别名来支持别名的自动发现。

它可以定制 Group 以提供此功能。如中所述 自定义组 ,一个组提供两种方法: list_commands()get_command() 。在这种情况下,您只需要覆盖后者,因为您通常不想列举帮助页面上的别名,以避免混淆。

下面的示例实现 Group 它接受命令的前缀。如果有一条命令叫 push ,它会接受 pus 作为别名(只要它是唯一的):

class AliasedGroup(click.Group):
    def get_command(self, ctx, cmd_name):
        rv = super().get_command(ctx, cmd_name)

        if rv is not None:
            return rv

        matches = [
            x for x in self.list_commands(ctx)
            if x.startswith(cmd_name)
        ]

        if not matches:
            return None

        if len(matches) == 1:
            return click.Group.get_command(self, ctx, matches[0])

        ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")

    def resolve_command(self, ctx, args):
        # always return the full command name
        _, cmd, args = super().resolve_command(ctx, args)
        return cmd.name, cmd, args

它的使用方法如下:

@click.group(cls=AliasedGroup)
def cli():
    pass

@cli.command
def push():
    pass

@cli.command
def pop():
    pass

请参阅 alias example 在Click的存储库中查看另一个示例。

参数修改

如您所见,参数(选项和参数)被转发到命令回调。防止将参数传递给回调的一种常见方法是 expose_value 参数的参数,它完全隐藏参数。它的工作方式是 Context 对象具有 params 属性,它是所有参数的字典。字典中的任何内容都将被传递给回调函数。

这可以用来补充其他参数。通常,不推荐使用此模式,但在某些情况下,它可能会很有用。至少,知道系统以这种方式工作是件好事。

import urllib

def open_url(ctx, param, value):
    if value is not None:
        ctx.params['fp'] = urllib.urlopen(value)
        return value

@click.command()
@click.option('--url', callback=open_url)
def cli(url, fp=None):
    if fp is not None:
        click.echo(f"{url}: {fp.code}")

在这种情况下,回调会返回未更改的URL,但也会传递一秒钟 fp 回调的值。更推荐的是在包装器中传递信息,但是:

import urllib

class URL(object):

    def __init__(self, url, fp):
        self.url = url
        self.fp = fp

def open_url(ctx, param, value):
    if value is not None:
        return URL(value, urllib.urlopen(value))

@click.command()
@click.option('--url', callback=open_url)
def cli(url):
    if url is not None:
        click.echo(f"{url.url}: {url.fp.code}")

令牌规范化

Changelog

在 2.0 版本加入.

从click 2.0开始,可以提供用于规范化令牌的函数。标记是选项名、选项值或命令值。例如,这可以用于实现不区分大小写的选项。

为了使用这个特性,需要向上下文传递一个执行令牌规范化的函数。例如,您可以有一个将令牌转换为小写的函数:

CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower())

@click.command(context_settings=CONTEXT_SETTINGS)
@click.option('--name', default='Pete')
def cli(name):
    click.echo(f"Name: {name}")

以及它在命令行上的工作方式:

$ cli --NAME=Pete
Name: Pete

调用其他命令

有时,从另一个命令调用一个命令可能很有趣。这是一种通常不鼓励单击的模式,但仍然可能。为此,您可以使用 Context.invoke()Context.forward() 方法。

它们的工作原理相似,但不同之处在于 Context.invoke() 只使用作为调用者提供的参数调用另一个命令,而 Context.forward() 从当前命令填充参数。两者都接受命令作为第一个参数,其他的一切都会如您所期望的那样向前传递。

例子:

cli = click.Group()

@cli.command()
@click.option('--count', default=1)
def test(count):
    click.echo(f'Count: {count}')

@cli.command()
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
    ctx.forward(test)
    ctx.invoke(test, count=42)

看起来像是:

$ cli dist
Count: 1
Count: 42

回调评估顺序

click的工作方式与其他一些命令行解析器稍有不同,因为它试图在调用任何回调之前,将程序员定义的参数顺序与用户定义的参数顺序进行协调。

当从optparse或其他系统移植复杂的模式时,这是需要理解的一个重要概念。optparse中的参数回调调用作为解析步骤的一部分进行,而click中的回调调用在解析之后进行。

主要的区别在于,在optparse中,调用回调时使用原始值,而在值完全转换后调用click中的回调。

通常,调用顺序由用户向脚本提供参数的顺序驱动;如果有一个调用的选项 --foo 还有一个选择 --bar 用户称之为 --bar --foo ,然后回调 bar 会先开火 foo .

这条规则有三个例外,需要知道:

Eagerness:

一个选项可以设置为“热切”。所有热切参数在所有非热切参数之前进行评估,但同样按照用户在命令行上提供的顺序进行评估。

这对于像这样执行和退出的参数很重要 --help--version . 这两个参数都是很好的参数,但是命令行中首先出现的任何参数都将赢得并退出程序。

重复参数:

如果一个选项或参数在命令行上被拆分成多个位置,因为它是重复的——例如, --exclude foo --include baz --exclude bar --回调将根据第一个选项的位置触发。在这种情况下,回调将在 exclude 这两个选项都将通过 (foobar ,然后回调 include 将火与 baz 只有。

请注意,即使参数不允许多个版本,单击仍将接受第一个的位置,但它将忽略除最后一个以外的所有值。原因是允许通过设置默认值的Shell别名实现可组合性。

缺少参数:

如果在命令行上未定义参数,则回调仍将激发。这与它在optparse中的工作方式不同,在optparse中,未定义的值不会触发回调。缺少的参数会在最末端触发它们的回调,这使得它们可以默认为来自以前的参数的值。

大多数情况下,您不需要担心其中的任何一个问题,但是了解它如何在某些高级案例中工作是很重要的。

转发未知选项

在某些情况下,能够接受所有未知选项进行进一步的手动处理是很有趣的。从click 4.0开始,click通常可以做到这一点,但它有一些局限性,这些局限性在于问题的性质。通过一个名为 ignore_unknown_options 它将指示解析器收集所有未知选项,并将它们放入剩余参数,而不是触发解析错误。

这通常可以通过两种不同的方式激活:

  1. 它可以在自定义上启用 Command 子类通过更改 ignore_unknown_options 属性。

  2. 它可以通过更改上下文类上相同名称的属性来启用 (Context.ignore_unknown_options )这是最好的改变通过 context_settings 命令上的字典。

对于大多数情况,最简单的解决方案是第二个。一旦行为改变了,就需要一些东西来选择那些剩余的选项(此时,这些选项被认为是参数)。对于这一点,您有两个选择:

  1. 你可以使用 pass_context() 以传递上下文。除此之外,只有当 ignore_unknown_options 你也设置 allow_extra_args 否则,该命令将中止,并返回一个错误,即存在剩余参数。如果使用此解决方案,将在 Context.args .

  2. 您可以附加一个 argument() 使用 nargs 设置为 -1 这将吞噬掉所有剩余的争论。在这种情况下,建议将 typeUNPROCESSED 为了避免对这些参数进行任何字符串处理,否则它们会被强制自动转换为Unicode字符串,这通常不是您想要的。

最后你会得到这样的结果:

import sys
from subprocess import call

@click.command(context_settings=dict(
    ignore_unknown_options=True,
))
@click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode')
@click.argument('timeit_args', nargs=-1, type=click.UNPROCESSED)
def cli(verbose, timeit_args):
    """A fake wrapper around Python's timeit."""
    cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
    if verbose:
        click.echo(f"Invoking: {' '.join(cmdline)}")
    call(cmdline)

看起来像是:

$ cli --help
Usage: cli [OPTIONS] [TIMEIT_ARGS]...

  A fake wrapper around Python's timeit.

Options:
  -v, --verbose  Enables verbose mode
  --help         Show this message and exit.

$ cli -n 100 'a = 1; b = 2; a * b'
python -mtimeit -n 100 a = 1; b = 2; a * b

$ cli -v 'a = 1; b = 2; a * b'
Invoking: echo python -mtimeit a = 1; b = 2; a * b
python -mtimeit a = 1; b = 2; a * b

正如您所看到的,冗长的标志是通过单击处理的,其他的一切都将结束在 timeit_args 用于进一步处理的变量,例如,它允许调用子进程。对于如何忽略未处理的标志,有一些重要的事情需要了解:

  • 未知的长选项通常被忽略,根本不进行处理。例如,如果 --foo=bar--foo bar 他们通常都是这样结束的。注意,由于解析器不知道选项是否接受参数,所以 bar 部分可以作为参数处理。

  • 如果需要,可能会部分处理和重新组装未知的短选项。例如,在上面的示例中,有一个名为 -v 这将启用详细模式。如果命令将被忽略 -va 然后 -v 部件将通过单击(如所知)处理,并且 -a 最终会在剩余参数中进行进一步处理。

  • 根据你的计划,你可以通过禁用分散的论点来获得一些成功。 (allow_interspersed_args )它指示解析器不允许混合参数和选项。根据您的情况,这可能会改善您的结果。

尽管通常不鼓励组合处理来自您自己的命令和来自另一个应用程序的命令的选项和参数,但如果您可以避免这种情况,您应该这样做。与其自己处理一些参数,不如将子命令下的所有内容都转发给另一个应用程序。

全局上下文访问

Changelog

在 5.0 版本加入.

从click 5.0开始,可以通过使用 get_current_context() 返回它的函数。这主要用于访问上下文绑定对象以及存储在其上的一些标志,以自定义运行时行为。例如 echo() 函数执行此操作以推断 color 旗帜。

示例用法:

def get_current_command_name():
    return click.get_current_context().info_name

应该注意的是,这只在当前线程内有效。如果生成其他线程,那么这些线程将无法引用当前上下文。如果要让另一个线程能够引用此上下文,则需要将该线程中的上下文用作上下文管理器::

def spawn_thread(ctx, func):
    def wrapper():
        with ctx:
            func()
    t = threading.Thread(target=wrapper)
    t.start()
    return t

现在,线程函数可以像主线程那样访问上下文。但是,如果您确实使用它进行线程处理,那么您需要非常小心,因为绝大多数上下文都不是线程安全的!您只能从上下文中读取,但不能对其执行任何修改。

检测参数源

在某些情况下,了解选项或参数是否来自命令行、环境、默认值或 Context.default_map . 这个 Context.get_parameter_source() 方法可以找出这一点。它将返回 ParameterSource 枚举。

@click.command()
@click.argument('port', nargs=1, default=8080, envvar="PORT")
@click.pass_context
def cli(ctx, port):
    source = ctx.get_parameter_source("port")
    click.echo(f"Port came from {source.name}")
$ cli 8080
Port came from COMMANDLINE

$ export PORT=8080
$ cli
Port came from ENVIRONMENT

$ cli
Port came from DEFAULT

管理资源

打开组中的资源可供子命令使用,这很有用。许多类型的资源在使用后需要关闭或以其他方式清理。在Python中实现这一点的标准方法是将上下文管理器与 with 语句。

例如, Repo 类从 复杂的应用 实际上可能定义为上下文管理器:

class Repo:
    def __init__(self, home=None):
        self.home = os.path.abspath(home or ".")
        self.db = None

    def __enter__(self):
        path = os.path.join(self.home, "repo.db")
        self.db = open_database(path)

    def __exit__(self, exc_type, exc_value, tb):
        self.db.close()

通常,它将与 with 声明:

with Repo() as repo:
    repo.db.query(...)

但是,一个 with 组中的块将退出并关闭数据库,然后才能被子命令使用。

相反,使用上下文的 with_resource() 方法进入上下文管理器并返回资源。当组和任何子命令完成时,上下文的资源将被清理。

@click.group()
@click.option("--repo-home", default=".repo")
@click.pass_context
def cli(ctx, repo_home):
    ctx.obj = ctx.with_resource(Repo(repo_home))

@cli.command()
@click.pass_obj
def log(obj):
    # obj is the repo opened in the cli group
    for entry in obj.db.query(...):
        click.echo(entry)

如果资源不是上下文管理器,通常可以使用 contextlib . 如果不可能,使用上下文 call_on_close() 方法注册清理函数。

@click.group()
@click.option("--name", default="repo.db")
@click.pass_context
def cli(ctx, repo_home):
    ctx.obj = db = open_db(repo_home)

    @ctx.call_on_close
    def close_db():
        db.record_use()
        db.save()
        db.close()