高级模式 — 单击文档
高级模式
除了库本身实现的常见功能外,还有无数模式可以通过扩展 Click 来实现。 这个页面应该对可以完成的事情有一些了解。
命令别名
许多工具支持命令别名(请参阅 命令别名示例 )。 例如,您可以配置 git
以接受 git ci
作为 git commit
的别名。 其他工具还通过自动缩短别名来支持自动发现别名。
Click 不支持开箱即用,但自定义 Group 或任何其他 MultiCommand 以提供此功能非常容易。
如 自定义多命令 中所述,多命令可以提供两种方法:list_commands() 和 get_command()。 在这种特殊情况下,您只需要覆盖后者,因为您通常不想在帮助页面上枚举别名以避免混淆。
以下示例实现了 Group 的子类,该子类接受命令的前缀。 如果有一个名为 push
的命令,它会接受 pus
作为别名(只要它是唯一的):
然后它可以像这样使用:
参数修改
如您所见,参数(选项和参数)被转发到命令回调。 防止参数传递给回调的一种常见方法是将 expose_value 参数传递给完全隐藏参数的参数。 其工作方式是 Context 对象有一个 params 属性,它是所有参数的字典。 该字典中的任何内容都将传递给回调。
这可用于组成附加参数。 通常不推荐使用这种模式,但在某些情况下它可能很有用。 至少知道系统以这种方式工作是件好事。
在这种情况下,回调会返回 URL 不变,但还会将第二个 fp
值传递给回调。 更推荐的是在包装器中传递信息:
令牌规范化
2.0 版中的新功能。
从 Click 2.0 开始,可以提供用于规范化令牌的函数。 标记是选项名称、选择值或命令值。 例如,这可用于实现不区分大小写的选项。
为了使用此功能,需要向上下文传递一个执行令牌规范化的函数。 例如,您可以有一个将标记转换为小写的函数:
以及它如何在命令行上工作:
调用其他命令
有时,从另一个命令调用一个命令可能很有趣。 这是 Click 通常不鼓励使用的模式,但仍有可能。 为此,您可以使用 Context.invoke() 或 Context.forward() 方法。
它们的工作方式类似,但不同之处在于 Context.invoke() 仅使用您作为调用者提供的参数调用另一个命令,而 Context.forward() 填充来自当前命令。 两者都接受命令作为第一个参数,其他所有内容都按照您的预期传递。
例子:
以及它的样子:
回调评估顺序
Click 的工作方式与其他一些命令行解析器略有不同,因为它尝试在调用任何回调之前将程序员定义的参数顺序与用户定义的参数顺序进行协调。
在将复杂模式从 optparse 或其他系统移植到 Click 时,这是一个需要理解的重要概念。 optparse 中的参数回调调用作为解析步骤的一部分发生,而 Click 中的回调调用发生在解析之后。
主要区别在于,在 optparse 中,回调在原始值发生时调用,而 Click 中的回调在值完全转换后调用。
通常,调用顺序由用户向脚本提供参数的顺序决定; 如果有一个名为 --foo
的选项和一个名为 --bar
的选项,并且用户将其称为 --bar --foo
,则 bar
的回调将在该回调之前触发对于 foo
。
此规则有以下三个例外情况,您必须了解这些例外情况:
- 渴望:
一个选项可以设置为“eager”。 所有 Eager 参数都在所有非 Eager 参数之前进行评估,但再次按照用户在命令行中提供的顺序进行评估。
这对于像
--help
和--version
这样执行和退出的参数很重要。 两者都是急切参数,但是命令行上第一个出现的参数将获胜并退出程序。- 重复参数:
如果一个选项或参数在命令行上因为重复而被分成多个位置——例如,
--exclude foo --include baz --exclude bar
——回调将根据第一个选项的位置触发。 在这种情况下,回调将触发exclude
并传递两个选项(foo
和bar
),然后include
的回调将触发仅限baz
。请注意,即使一个参数不允许多个版本,Click 仍会接受第一个的位置,但会忽略除最后一个之外的所有值。 这样做的原因是通过设置默认值的 shell 别名允许可组合性。
- 缺少参数:
如果命令行上未定义参数,回调仍将触发。 这与它在 optparse 中的工作方式不同,其中未定义的值不会触发回调。 缺少的参数会在最后触发它们的回调,这使得它们可以默认为来自之前参数的值。
大多数情况下,您不需要担心任何这些,但了解它在某些高级情况下的工作原理很重要。
转发未知选项
在某些情况下,能够接受所有未知选项以进行进一步的手动处理是很有趣的。 从 Click 4.0 开始,Click 通常可以做到这一点,但由于问题的性质,它有一些限制。 对此的支持是通过名为 ignore_unknown_options
的解析器标志提供的,它将指示解析器收集所有未知选项并将它们放入剩余的参数中,而不是触发解析错误。
这通常可以通过两种不同的方式激活:
- 它可以通过更改 ignore_unknown_options 属性在自定义 Command 子类上启用。
- 可以通过更改上下文类 (Context.ignore_unknown_options) 上的同名属性来启用它。 最好通过命令中的
context_settings
字典来更改。
对于大多数情况,最简单的解决方案是第二种。 一旦行为发生变化,就需要一些东西来获取那些剩余的选项(此时被视为参数)。 为此,您有两个选择:
- 您可以使用 pass_context() 来获取传递的上下文。 这仅在除了 ignore_unknown_options 之外还设置 allow_extra_args 时才有效,否则该命令将因存在剩余参数的错误而中止。 如果你使用这个解决方案,额外的参数将被收集在 Context.args 中。
- 您可以附加一个 argument() 并将
nargs
设置为 -1,这将消耗所有剩余的参数。 在这种情况下,建议将 type 设置为 UNPROCESSED 以避免对这些参数进行任何字符串处理,否则它们会被自动强制转换为 unicode 字符串,这通常不是您想要的。
最后你会得到这样的结果:
以及它的样子:
正如您所看到的,详细标志由 Click 处理,其他所有内容最终都在 timeit_args 变量中进行进一步处理,例如,允许调用子进程。 关于忽略未处理标志是如何发生的,有一些重要的事情需要了解:
- 未知的长选项通常会被忽略并且根本不处理。 因此,例如,如果通过
--foo=bar
或--foo bar
,它们通常会以这样的方式结束。 请注意,由于解析器无法知道选项是否接受参数,因此bar
部分可能会作为参数处理。 - 如有必要,可能会部分处理和重新组合未知的短期期权。 例如,在上面的示例中,有一个名为
-v
的选项可以启用详细模式。 如果命令将被-va
忽略,则-v
部分将由 Click 处理(众所周知),而-a
将在剩余参数中结束以供进一步处理. - 根据您的计划,您可能会通过禁用散在参数 (allow_interspersed_args) 来取得一些成功,该参数指示解析器不允许混合参数和选项。 根据您的情况,这可能会改善您的结果。
通常,尽管不鼓励对来自您自己的命令和来自其他应用程序的命令的选项和参数进行组合处理,但如果可以避免,则应该这样做。 将子命令下的所有内容转发到另一个应用程序比自己处理一些参数要好得多。
全局上下文访问
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 枚举的成员。
管理资源
打开组中的资源以供子命令使用会很有用。 许多类型的资源在使用后需要关闭或以其他方式清理。 在 Python 中执行此操作的标准方法是使用带有 with
语句的上下文管理器。
例如,来自 Complex Applications 的 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()