一 click
1介绍
click 是一个以尽可能少的代码、以组合的方式创建优美的命令行程序的 Python 包。它有很高的可配置性,同时也能开箱即用。
它旨在让编写命令行工具的过程既快速又有趣,还能防止由于无法实现预期的 CLI API 所产生挫败感。它有如下三个特点:
- 任意嵌套命令
- 自动生成帮助
- 支持运行时延迟加载子命令
2 快速开始
2.1 业务逻辑
假设命令行程序的输入是 name
和 count
,功能是打印指定次数的名字。
那么在 hello.py
中,很容易写出如下代码:
1 | def hello(count, name): |
这段代码的逻辑很简单,就是循环 count
次,使用 click.echo
打印 name
。其中,click.echo
和 print
的作用相似,但功能更加强大,能处理好 Unicode 和 二进制数据的情况。
2.2 定义参数
很显然,我们需要针对 count
和 name
来定义它们所对应的参数信息。
count
对应为命令行选项--count
,类型为数字,我们希望在不提供参数时,其默认值是 1name
对应为命令行选项--name
,类型为字符串,我们希望在不提供参数时,能给人提示
使用 click
,就可以写成下面这样:
1 | from click import click |
在上面的示例中:
- 使用装饰器的方式,即定义了参数,又将之与处理逻辑绑定,这真是优雅。和
argparse
、docopt
比起来,就少了一步绑定过程 - 使用
click.command
表示hello
是对命令的处理 - 使用
1 | click.option |
来定义参数选项
- 对于
--count
来说,使用default
来指定默认值。而由于默认值是数字,进而暗示--count
选项的类型为数字 - 对于
--name
来说,使用prompt
来指定未输入该选项时的提示语 - 使用
help
来指定帮助信息
不论是装饰器的方式、还是各种默认行为,click
都是像它的介绍所说的那样,让人尽可能少地编写代码,让整个过程变得快速而有趣。
2.3 代码梳理
使用 click
的方式非常简单,我们将上文的代码汇总下,以有一个更清晰的认识:
1 | # hello.py |
若我们指定次数和名字:
1 | python3 hello.py --count 2 --name Eric |
若我们什么都不指定,则会提示输入名字,并默认输出一次:
1 | python3 hello.py |
我们还可以通过 --help
参数查看自动生成的帮助信息:
1 | Usage: hello.py [OPTIONS] |
3 深入click
在概念上, click
把命令行分为 3 个组成:参数、选项和命令。
参数
就是跟在命令后的除选项外的内容,比如git add a.txt
中的a.txt
就是表示文件路径的参数选项
就是以-
或--
开头的参数,比如-f
、--file
命令
就是命令行的初衷了,比如git
就是命令,而git add
中的add
则是git
的子命令
3.1 参数
3.1.1 基本参数
基本参数
就是通过位置里指定参数值。
比如,我们可以指定两个位置参数 x
和 y
,先添加的 x
位于第一个位置,后加入的 y
位于第二个位置。那么在命令行中输入 1 2
的时候,分别对应到的就是 x
和 y
:
1 |
|
3.1.2 参数类型
参数类型
就是将参数值作为什么类型去解析,默认情况下是字符串类型。我们可以通过 type
入参来指定参数类型。
click
支持的参数类型多种多样:
str
/click.STRING
表示字符串类型,这也是默认类型int
/click.INT
表示整型float
/click.FLOAT
表示浮点型bool
/click.BOOL
表示布尔型。很棒之处在于,它会识别表示真/假的字符。对于1
、yes
、y
和true
会转化为True
;0
、no
、n
和false
会转化为False
click.UUID
表示 UUID,会自动将参数转换为uuid.UUID
对象click.FILE
表示文件,会自动将参数转换为文件对象,并在命令行结束时自动关闭文件click.PATH
表示路径click.Choice
表示选择选项click.IntRange
表示范围选项
同 argparse
一样,click
也支持自定义类型,需要编写 click.ParamType
的子类,并重载 convert
方法。
官网提供了一个例子,实现了一个整数类型,除了普通整数之外,还接受十六进制和八进制数字, 并将它们转换为常规整数:
1 | class BasedIntParamType(click.ParamType): |
3.1.3 文件参数
在基本参数的基础上,通过指定参数类型,我们就能构建出各类参数。
文件参数
是非常常用的一类参数,通过 type=click.File
指定,它能正确处理所有 Python 版本的 unicode 和 字节,使得处理文件十分方便。
1 |
|
3.1.4文件路径参数
文件路径参数
用来处理文件路径,可以对路径做是否存在等检查,通过 type=click.Path
指定。不论文件名是 unicode 还是字节类型,获取到的参数类型都是 unicode 类型。
1 |
|
如果文件名是以 -
开头,会被误认为是命令行选项,这个时候需要在参数前加上 --
和空格,比如
1 | $ python hello.py -- -foo.txt |
3.1.5选择项参数
选择项参数
用来限定参数内容,通过 type=click.Choice
指定。
比如,指定文件读取方式限制为 read-only
和 read-write
:
1 |
|
3.1.6可变参数
可变参数
用来定义一个参数可以有多个值,且能通过 nargs
来定义值的个数,取得的参数的变量类型为元组。
若 nargs=N
,N
为一个数字,则要求该参数提供 N 个值。若 N
为 -1
则接受提供无数量限制的参数,如:
1 |
|
如果要实现 argparse
中要求参数数量为 1 个或多个的功能,则指定 nargs=-1
且 required=True
即可:
1 |
|
3.1.7从环境变量读取参数
通过在 click.argument
中指定 envvar
,则可读取指定名称的环境变量作为参数值,比如:
1 |
|
执行如下命令查看效果:
1 | FILENAME=hello.txt python3 hello.py |
3.2 选项
通过 click.option
可以给命令增加选项,并通过配置函数的参数来配置不同功能的选项。
3.2.1 给选项命名
click.option
中的命令规则可参考参数名称。它接受的前两个参数为长、短选项(顺序随意),其中:
- 长选项以 “–” 开头,比如 “–string-to-echo”
- 短选项以 “-” 开头,比如 “-s”
第三个参数为选项参数的名称,如果不指定,将会使用长选项的下划线形式名称:
1 |
|
显示指定为 string
1 |
|
3.2.2 基本值选项
值选项是非常常用的选项,它接受一个值。如果在命令行中提供了值选项,则需要提供对应的值;反之则使用默认值。若没在 click.option
中指定默认值,则默认值为 None
,且该选项的类型为 STRING;反之,则选项类型为默认值的类型。
比如,提供默认值为 1,则选项类型为 INT:
1 |
|
如果要求选项为必填,则可指定 click.option
的 required=True
:
1 |
|
如果选项名称和 Python 中的关键字冲突,则可以显式的指定选项名称。比如将 --from
的名称设置为 from_
:
1 |
|
如果要在帮助中显式默认值,则可指定 click.option
的 show_default=True
:
1 |
|
在命令行中调用则有:
1 | $ dots --help |
3.2.3多值选项
有时,我们会希望命令行中一个选项能接收多个值,通过指定 click.option
中的 nargs
参数(必须是大于等于 0)。这样,接收的多值选项就会变成一个元组。
比如,在下面的示例中,当通过 --pos
指定多个值时,pos
变量就是一个元组,里面的每个元素是一个 float
:
1 |
|
在命令行中调用则有:
1 | $ findme --pos 2.0 3.0 |
有时,通过同一选项指定的多个值得类型可能不同,这个时候可以指定 click.option
中的 type=(类型1, 类型2, ...)
来实现。而由于元组的长度同时表示了值的数量,所以就无须指定 nargs
参数。
1 |
|
在命令行中调用则有:
1 | $ putitem --item peter 1338 |
3.2.4多选项
不同于多值选项是通过一个选项指定多个值,多选项则是使用多个相同选项分别指定值,通过 click.option
中的 multiple=True
来实现。
当我们定义如下多选项:
1 |
|
便可以指定任意数量个选项来指定值,获取到的 message
是一个元组:
1 | $ commit -m foo -m bar --message baz |
3.2.5 计值选项
有时我们可能需要获得选项的数量,那么可以指定 click.option
中的 count=True
来实现。
最常见的使用场景就是指定多个 --verbose
或 -v
选项来表示输出内容的详细程度。
1 |
|
在命令行中调用则有:
1 | $ log -vvv |
通过上面的例子,verbose
就是数字,表示 -v
选项的数量,由此可以进一步使用该值来控制日志的详细程度。
2.6 布尔选项
布尔选项用来表示真或假,它有多种实现方式:
- 通过
click.option
的is_flag=True
参数来实现:
1 | import sys |
在命令行中调用则有:
1 | $ info --shout |
- 通过在
click.option
的选项定义中使用/
分隔表示真假两个选项来实现:
1 | import sys |
在命令行中调用则有:
1 | $ info --shout |
在 Windows 中,一个选项可以以 /
开头,这样就会真假选项的分隔符冲突了,这个时候可以使用 ;
进行分隔:
1 |
|
在 cmd 中调用则有:
1 | > log /debug |
3.2.7 特性切换选项
所谓特性切换就是切换同一个操作对象的不同特性,比如指定 --upper
就让输出大写,指定 --lower
就让输出小写。这么来看,布尔值其实是特性切换的一个特例。
要实现特性切换选项,需要让多个选项都有相同的参数名称,并且定义它们的标记值 flag_value
:
1 | import sys |
在命令行中调用则有:
1 | $ info --upper |
在上面的示例中,--upper
和 --lower
都有相同的参数值 transformation
:
- 当指定
--upper
时,transformation
就是--upper
选项的标记值upper
- 当指定
--lower
时,transformation
就是--lower
选项的标记值lower
进而就可以做进一步的业务逻辑处理。
3.2.8 选择项选项
选择项选项
和 上篇文章中介绍的 选择项参数
类似,只不过是限定选项内容,依旧是通过 type=click.Choice
实现。此外,case_sensitive=False
还可以忽略选项内容的大小写。
1 |
|
在命令行中调用则有:
1 | $ digest --hash-type=MD5 |
3.2.9 提示选项
顾名思义,当提供了选项却没有提供对应的值时,会提示用户输入值。这种交互式的方式会让命令行变得更加友好。通过指定 click.option
中的 prompt
可以实现。
- 当
prompt=True
时,提示内容为选项的参数名称
1 |
|
在命令行调用则有:
1 | $ hello --name=John |
- 当
prompt='Your name please'
时,提示内容为指定内容
1 |
|
在命令行中调用则有:
1 | $ hello |
基于提示选项,我们还可以指定 hide_input=True
来隐藏输入,confirmation_prompt=True
来让用户进行二次输入,这非常适合输入密码的场景。
1 |
|
当然,也可以直接使用 click.password_option
:
1 |
|
我们还可以给提示选项设置默认值,通过 default
参数进行设置,如果被设置为函数,则可以实现动态默认值。
1 |
|
详情请阅读 Dynamic Defaults for Prompts。
3.2.10 范围选项
如果希望选项的值在某个范围内,就可以使用范围选项,通过指定 type=click.IntRange
来实现。它有两种模式:
- 默认模式(非强制模式),如果值不在区间范围内将会引发一个错误。如
type=click.IntRange(0, 10)
表示范围是 [0, 10],超过该范围报错 - 强制模式,如果值不在区间范围内,将会强制选取一个区间临近值。如
click.IntRange(0, None, clamp=True)
表示范围是 [0, +∞),小于 0 则取 0,大于 20 则取 20。其中None
表示没有限制
1 |
|
在命令行中调用则有:
1 | $ repeat --count=1000 --digit=5 |
3.2.11 回调和优先
回调 通过 click.option
中的 callback
可以指定选项的回调,它会在该选项被解析后调用。回调函数的签名如下:
1 | def callback(ctx, param, value): |
其中:
- ctx 是命令的上下文 click.Context
- param 为选项变量 click.Option
- value 为选项的值
使用回调函数可以完成额外的参数校验逻辑。比如,通过 –rolls 的选项来指定摇骰子的方式,内容为“d”,表示 M 面的骰子摇 N 次,N 和 M 都是数字。在真正的处理 rolls 前,我们需要通过回调函数来校验它的格式:
1 | def validate_rolls(ctx, param, value): |
这样,当我们输入错误格式时,变会校验不通过:
1 | $ roll --rolls=42 |
输入正确格式时,则正常输出信息:
1 | $ roll --rolls=2d12 |
优先 通过 click.option
中的 is_eager
可以让该选项成为优先选项,这意味着它会先于所有选项处理。
利用回调和优先选项,我们就可以很好地实现 --version
选项。不论命令行中写了多少选项和参数,只要包含了 --version
,我们就希望它打印版本就退出,而不执行其他选项的逻辑,那么就需要让它成为优先选项,并且在回调函数中打印版本。
此外,在 click
中每个选项都对应到命令处理函数的同名参数,如果不想把该选项传递到处理函数中,则需要指定 expose_value=True
,于是有:
1 | def print_version(ctx, param, value): |
当然 click
提供了便捷的 click.version_option
来实现 --version
:
1 |
|
3.2.12 Yes 选项
基于前面的学习,我们可以实现 Yes 选项,也就是对于某些操作,不提供 --yes
则进行二次确认,提供了则直接操作:
1 | def abort_if_false(ctx, param, value): |
当然 click
提供了便捷的 click.confirmation_option
来实现 Yes 选项:
1 |
|
在命令行中调用则有:
1 | $ dropdb |
3.2.13 其他增强功能
click
支持从环境中读取选项的值,这是 argparse
所不支持的,可参阅官方文档的 Values from Environment Variables 和 Multiple Values from Environment Values。
click
支持指定选项前缀,你可以不使用 -
作为选项前缀,还可使用 +
或 /
,当然在一般情况下并不建议这么做。详情参阅官方文档的 Other Prefix Characters
3.3 命令和组
Click
中非常重要的特性就是任意嵌套命令行工具的概念,通过 Command 和 Group (实际上是 MultiCommand)来实现。
所谓命令组就是若干个命令(或叫子命令)的集合,也成为多命令。
3.3.1回调调用
对于一个普通的命令来说,回调发生在命令被执行的时候。如果这个程序的实现中只有命令,那么回调总是会被触发,就像我们在上一篇文章中举出的所有示例一样。不过像 --help
这类选项则会阻止进入回调。
对于组和多个子命令来说,情况略有不同。回调通常发生在子命令被执行的时候:
1 |
|
执行效果如下:
1 | Usage: tool.py [OPTIONS] COMMAND [ARGS]... |
在上面的示例中,我们将函数 cli
定义为一个组,把函数 sync
定义为这个组内的子命令。当我们调用 tool.py --debug sync
命令时,会依次触发 cli
和 sync
的处理逻辑(也就是命令的回调)。
3.3.2嵌套处理和上下文
从上面的例子可以看到,命令组 cli
接收的参数和子命令 sync
彼此独立。但是有时我们希望在子命令中能获取到命令组的参数,这就可以用 Context 来实现。
每当命令被调用时,click
会创建新的上下文,并链接到父上下文。通常,我们是看不到上下文信息的。但我们可以通过 pass_context 装饰器来显式让 click
传递上下文,此变量会作为第一个参数进行传递。
1 |
|
在上面的示例中:
- 通过为命令组
cli
和子命令sync
指定装饰器click.pass_context
,两个函数的第一个参数都是ctx
上下文 - 在命令组
cli
中,给上下文的obj
变量(字典)赋值 - 在子命令
sync
中通过ctx.obj['DEBUG']
获得上一步的参数 - 通过这种方式完成了从命令组到子命令的参数传递
3.3.3 不使用命令来调用命令组
默认情况下,调用子命令的时候才会调用命令组。而有时你可能想直接调用命令组,通过指定 click.group
的 invoke_without_command=True
来实现:
1 |
|
调用命令有:
1 | $ tool |
在上面的示例中,通过 ctx.invoked_subcommand
来判断是否由子命令触发,针对两种情况打印日志。
3.3.4 自定义命令组/多命令
除了使用 click.group 来定义命令组外,你还可以自定义命令组(也就是多命令),这样你就可以延迟加载子命令,这会很有用。
自定义多命令需要实现 list_commands
和 get_command
方法:
1 | import click |
3.3.5 合并命令组/多命令
当有多个命令组,每个命令组中有一些命令,你想把所有的命令合并在一个集合中时,click.CommandCollection
就派上了用场:
1 |
|
调用命令有:
1 | $ cli --help |
从上面的示例可以看出,cmd1
和 cmd2
分别属于 cli1
和 cli2
,通过 click.CommandCollection
可以将这些子命令合并在一起,将其能力提供个同一个命令程序。
Tips:如果多个命令组中定义了同样的子命令,那么取第一个命令组中的子命令。
3.3.6 链式命令组/多命令
有时单级子命令可能满足不了你的需求,你甚至希望能有多级子命令。典型地,setuptools
包中就支持多级/链式子命令: setup.py sdist bdist_wheel upload
。在 click 3.0 之后,实现链式命令组变得非常简单,只需在 click.group
中指定 chain=True
:
1 |
|
调用命令则有:
1 | $ setup.py sdist bdist_wheel |
3.3.7 命令组/多命令管道
链式命令组中一个常见的场景就是实现管道,这样在上一个命令处理好后,可将结果传给下一个命令处理。
实现命令组管道的要点是让每个命令返回一个处理函数,然后编写一个总的管道调度函数(并由 MultiCommand.resultcallback()
装饰):
1 |
|
在上面的示例中:
- 将
cli
定义为了链式命令组,并且指定 invoke_without_command=True,也就意味着可以不传子命令来触发命令组 - 定义了三个命令处理函数,分别对应
uppercase
、lowercase
和strip
命令 - 在管道调度函数
process_pipeline
中,将输入input
变成生成器,然后调用处理函数(实际输入几个命令,就有几个处理函数)进行处理
3.3.8 覆盖默认值
默认情况下,参数的默认值是从通过装饰器参数 default
定义。我们还可以通过 Context.default_map
上下文字典来覆盖默认值:
1 |
|
在上面的示例中,通过在 cli
中指定 default_map
变可覆盖命令(一级键)的选项(二级键)默认值(二级键的值)。
我们还可以在 click.group
中指定 context_settings
来达到同样的目的:
1 | CONTEXT_SETTINGS = dict( |
调用命令则有:
1 | $ cli runserver |
首先介绍了命令的回调调用、上下文,再进一步介绍命令组的自定义、合并、链接、管道等功能,了解到了 click
的强大。而命令组中更加高阶的能力(如命令返回值)则可看官方文档进一步了解。
我们通过介绍 click
的参数、选项和命令已经能够完全实现命令行程序的所有功能。而 click
还为我们提供了许多锦上添花的功能,比如实用工具、参数自动补全等
4 click增强
4.1 Bash 补全
Bash 补全是 click
提供的一个非常便捷和强大的功能,这是它比 argpase
和 docopt
强大的一个表现。
在命令行程序正确安装后,Bash 补全才可以使用。而如何安装可以参考 setup 集成。Click 目前仅支持 Bash 和 Zsh 的补全。
4.1.1 补全能力
通常来说,Bash 补全支持对子命令、选项、以及选项或参数值得补全。比如:
1 | $ repo <TAB><TAB> |
此外,click
还支持自定义补全,这在动态生成补全场景中很有用,使用 autocompletion
参数。autocompletion
需要指定为一个回调函数,并且返回字符串的列表。此函数接受三个参数:
ctx
—— 当前的 click 上下文args
传入的参数列表incomplete
正在补全的词
这里有一个根据环境变量动态生成补全的示例:
1 | import os |
在 ZSH
中,还支持补全帮助信息。只需将 autocompletion
回调函数中返回的字符串列表中的字符串改为二元元组,第一个元素是补全内容,第二个元素是帮助信息。
这里有一个颜色补全的示例:
1 | import os |
4.1.2 激活补全
要激活 Bash 的补全功能,就需要告诉它你的命令行程序有补全的能力。通常通过一个神奇的环境变量 __COMPLETE
来告知,其中 `` 是大写下划线形式的程序名称。
比如有一个命令行程序叫做 foo-bar
,那么对应的环境变量名称为 _FOO_BAR_COMPLETE
,然后在 .bashrc
中使用 source
导出即可:
1 | eval "$(_FOO_BAR_COMPLETE=source foo-bar)" |
或者在 .zshrc
中使用:
1 | eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)" |
不过上面的方式总是在命令行程序启动时调用,这可能在有多个程序时减慢 shell 激活的速度。另一种方式是把命令放在文件中,就像这样:
1 | # 针对 Bash |
然后把脚本文件路径加到 .bashrc
或 .zshrc
中:
1 | . /path/to/foo-bar-complete.sh |
.2 实用工具
4.2.1 打印到标准输出
echo() 函数可以说是最有用的实用工具了。它和 Python 的 print
类似,主要的区别在于它同时在 Python 2 和 3 中生效,能够智能地检测未配置正确的输出流,且几乎不会失败(除了 Python 3 中的少数限制。)
echo
即支持 unicode,也支持二级制数据,如:
1 | import click |
4.2.2 ANSI 颜色
有些时候你可能希望输出是有颜色的,这尤其在输出错误信息时有用,而 click
在这方面支持的很好。
首先,你需要安装 colorama
:
1 | pip install colorama |
然后,就可以使用 style() 函数来指定颜色:
1 | import click |
click
还提供了更加简便的函数 secho,它就是 echo
和 style
的组合:
1 | click.secho('Hello World!', fg='green') |
.2.3 分页支持
有些时候,命令行程序会输出长文本,但你希望能让用户盘也浏览。使用 echo_via_pager() 函数就可以轻松做到。
例如:
1 | def less(): |
如果输出的文本特别大,处于性能的考虑,希望翻页时生成对应内容,那么就可以使用生成器:
1 | def _generate_output(): |
4.2.4 清除屏幕
使用 clear() 可以轻松清除屏幕内容:
1 | import click |
.2.5 从终端获取字符
通常情况下,使用内建函数 input
或 raw_input
获得的输入是用户输出一段字符然后回车得到的。但在有些场景下,你可能想在用户输入单个字符时就能获取到并且做一定的处理,这个时候 getchar() 就派上了用场。
比如,根据输入的 y
或 n
做特定处理:
1 | import click |
4.2.6 等待按键
在 Windows 的 cmd 中我们经常看到当执行完一个命令后,提示按下任意键退出。通过使用 pause() 可以实现暂停直至用户按下任意键:
1 | import click |
4.2.7 启动编辑器
通过 edit() 可以自动启动编辑器。这在需要用户输入多行内容时十分有用。
在下面的示例中,会启动默认的文本编辑器,并在里面输入一段话:
1 | import click |
edit()
函数还支持打开特定文件,比如:
1 | import click |
.2.8 启动应用程序
通过 launch 可以打开 URL 或文件类型所关联的默认应用程序。如果设置 locate=True
,则可以启动文件管理器并自动选中特定文件。
示例:
1 | # 打开浏览器,访问 URL |
4.2.9 显示进度条
click
内置了 progressbar() 函数来方便地显示进度条。
它的用法也很简单,假定你有一个要处理的可迭代对象,处理完每一项就要输出一下进度,那么就有两种用法。
用法一:使用 progressbar
构造出 bar
对象,迭代 bar
对象来自动告知进度:
1 | import time |
用法二:使用 progressbar
构造出 bar
对象,迭代原始可迭代对象,并不断向 bar
更新进度:
1 | import time |
4.2.10 更多实用工具
5 总结
click
提供了非常多的增强型功能,本文着重介绍了它的 Bash 补全和十多个实用工具,这会让你在实现命令行的过程中如虎添翼。此外,click
还提供了诸如命令别名、参数修改、标准化令牌、调用其他命令、回调顺序等诸多高级模式 以应对更加复杂或特定的场景,我就不再深入介绍。
6 实战
6.1 使用click实现调用Java的jar包
1 | #!/usr/bin/python |
7 将python写的命令行打包,并安装到linux上
1 安装setuptools
1 | pip install setuptools |
更新:
1 | pip install --upgrade setuptools |
2 新建python的项目
目录结构:
1 | --python-dev |
Setup.py:
1 | #coding:utf-8 |
3 打包
1 | python setup.py bdist_egg |
4 Linux安装egg包
1 | easy_install *.egg |