Bootstrap

Python 命令行参数:Argparse 与 Click

作者:高玉涵
时间:2022.7.29 10:15
博客:blog.csdn.net/cg_i

简介

 和以往一样,我在实践过程中,执行编写的程序时,通过命令行传值给 Python 程序,达到从外部控制程序(而不是在代码内对这些值进行硬编码)。Python 内置了 Argparse 的标准库用于创建命令行,Argparse 是面向过程的,需要先设置解析器,再定义参数,再解析命令行,最后实现业务逻辑。在一些人看来,这此方式都不够优雅。

 而 Click 则是用一种你很熟知的方式来玩转命令行。命令行程序本质上是定义参数和处理参数,而处理参数的逻辑一定与所定义的参数有关联的。那可不可以用函数和装饰器来实现处理参数逻辑与定义参数的关联呢?而 Click 正好就是以这种方式来使用的。

注解: 还有另外两个模块可以完成同样的任务,称为 getopt (对应于 C 语言中的 getopt() 函数) 和被弃用的 optparse。还要注意 argparse 是基于 optparse 的,因此用法与其非常相似。

一、Argparse 模块

1.1 概念

 让我们利用 ls 命令来展示我们将要在这篇入门教程中探索的功能:

$ ls
cpython  devguide  prog.py  pypy  rm-unused-function.patch
$ ls pypy
ctypes_configure  demo  dotviewer  include  lib_pypy  lib-python ...
$ ls -l
total 20
drwxr-xr-x 19 wena wena 4096 Feb 18 18:51 cpython
drwxr-xr-x  4 wena wena 4096 Feb  8 12:04 devguide
-rwxr-xr-x  1 wena wena  535 Feb 19 00:05 prog.py
drwxr-xr-x 14 wena wena 4096 Feb  7 00:59 pypy
-rw-r--r--  1 wena wena  741 Feb 18 01:01 rm-unused-function.patch
$ ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
...

我们可以从这四个命令中学到几个概念:

  • ls 是一个即使在运行的时候没有提供任何选项,也非常有用的命令。在默认情况下他会输出当前文件夹包含的文件和文件夹。

  • 如果我们想要使用比它默认提供的更多功能,我们需要告诉该命令更多信息。在这个例子里,我们想要查看一个不同的目录,pypy。我们所做的是指定所谓的位置参数。之所以这样命名,是因为程序应该如何处理该参数值,完全取决于它在命令行出现的位置。更能体现这个概念的命令如 cp,它最基本的用法是 cp SRC DEST。第一个位置参数指的是你想要复制的,第二个位置参数指的是你想要复制到的位置

  • 现在假设我们想要改变这个程序的行为。在我们的例子中,我们不仅仅只是输出每个文件的文件名,还输出了更多信息。在这个例子中,-l 被称为可选参数。

  • 这是一段帮助文档的文字。它是非常有用的,因为当你遇到一个你从未使用过的程序时,你可以通过阅读它的帮助文档来弄清楚它是如何运行的。

1.2 基础

 让我们从一个简单到(几乎)什么也做不了的例子开始:

import argparse
parser = argparse.ArgumentParser()
parser.parse_args()

以下是该代码的运行结果:

$ python3 prog.py
$ python3 prog.py --help
usage: prog.py [-h]

options:
  -h, --help  show this help message and exit
$ python3 prog.py --verbose
usage: prog.py [-h]
prog.py: error: unrecognized arguments: --verbose
$ python3 prog.py foo
usage: prog.py [-h]
prog.py: error: unrecognized arguments: foo

 程序运行情况如下:

  • 在没有任何选项的情况下运行脚本不会在标准输出显示任何内容。这没有什么用处。

  • 第二行代码开始展现出 argparse 模块的作用。我们几乎什么也没有做,但已经得到一条很好的帮助信息。

  • --help 选项,也可缩写为 -h,是唯一一个可以直接使用的选项(即不需要指定该选项的内容)。指定任何内容都会导致错误。即便如此,我们也能直接得到一条有用的用法信息。

1.3 位置参数介绍

 举个例子:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print(args.echo)

 运行此程序:

$ python3 prog.py
usage: prog.py [-h] echo
prog.py: error: the following arguments are required: echo
$ python3 prog.py --help
usage: prog.py [-h] echo

positional arguments:
  echo

options:
  -h, --help  show this help message and exit
$ python3 prog.py foo
foo

 程序运行情况如下:

  • 我们增加了 add_argument() 方法,该方法用于指定程序能够接受哪些命令行选项。在这个例子中,我将选项命名为 echo,与其功能一致。

  • 现在调用我们的程序必须要指定一个选项。

  • The parse_args() method actually returns some data from the options specified, in this case, echo.

  • 这一变量是 argparse 免费施放的某种 “魔法”(即是说,不需要指定哪个变量是存储哪个值的)。你也可以注意到,这一名称与传递给方法的字符串参数一致,都是 echo

 然而请注意,尽管显示的帮助看起来清楚完整,但它可以比现在更有帮助。比如我们可以知道 echo 是一个位置参数,但我们除了靠猜或者看源代码,没法知道它是用来干什么的。所以,我们可以把它改造得更有用:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo", help="echo the string you use here")
args = parser.parse_args()
print(args.echo)

 然后我们得到:

$ python3 prog.py -h
usage: prog.py [-h] echo

positional arguments:
  echo        echo the string you use here

options:
  -h, --help  show this help message and exit

 现在,来做一些更有用的事情:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number")
args = parser.parse_args()
print(args.square**2)

 以下是该代码的运行结果:

$ python3 prog.py 4
Traceback (most recent call last):
  File "prog.py", line 5, in <module>
    print(args.square**2)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

 进展不太顺利。那是因为 argparse 会把我们传递给它的选项视作为字符串,除非我们告诉它别这样。所以,让我们来告诉 argparse 来把这一输入视为整数:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number",
                    type=int)
args = parser.parse_args()
print(args.square**2)

 以下是该代码的运行结果:

$ python3 prog.py 4
16
$ python3 prog.py four
usage: prog.py [-h] square
prog.py: error: argument square: invalid int value: 'four'

 做得不错。当这个程序在收到错误的无效的输入时,它甚至能在执行计算之前先退出,还能显示很有帮助的错误信息。

1.4 可选参数介绍

 到目前为止,我们一直在研究位置参数。让我们看看如何添加可选的:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="increase output verbosity")
args = parser.parse_args()
if args.verbosity:
    print("verbosity turned on")

 和输出:

$ python3 prog.py --verbosity 1
verbosity turned on
$ python3 prog.py
$ python3 prog.py --help
usage: prog.py [-h] [--verbosity VERBOSITY]

options:
  -h, --help            show this help message and exit
  --verbosity VERBOSITY
                        increase output verbosity
$ python3 prog.py --verbosity
usage: prog.py [-h] [--verbosity VERBOSITY]
prog.py: error: argument --verbosity: expected one argument

 程序运行情况如下:

  • 这一程序被设计为当指定 --verbosity 选项时显示某些东西,否则不显示。

  • 不添加这一选项时程序没有提示任何错误而退出,表明这一选项确实是可选的。注意,如果一个可选参数没有被使用时,相关变量被赋值为 None,在此例中是 args.verbosity,这也就是为什么它在 if 语句中被当作逻辑假。

  • 帮助信息有点不同。

  • 使用 --verbosity 选项时,必须指定一个值,但可以是任何值。

 上述例子接受任何整数值作为 --verbosity 的参数,但对于我们的简单程序而言,只有两个值有实际意义:True 或者 False。让我们据此修改代码:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", help="increase output verbosity",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

 和输出:

$ python3 prog.py --verbose
verbosity turned on
$ python3 prog.py --verbose 1
usage: prog.py [-h] [--verbose]
prog.py: error: unrecognized arguments: 1
$ python3 prog.py --help
usage: prog.py [-h] [--verbose]

options:
  -h, --help  show this help message and exit
  --verbose   increase output verbosity

 程序运行情况如下:

  • 现在,这一选项更多地是一个标志,而非需要接受一个值的什么东西。我们甚至改变了选项的名字来符合这一思路。注意我们现在指定了一个新的关键词 action,并赋值为 "store_true"。这意味着,当这一选项存在时,为 args.verbose 赋值为 True。没有指定时则隐含地赋值为 False

  • 当你为其指定一个值时,它会报错,符合作为标志的真正的精神。

  • 留意不同的帮助文字。

1.5 短选项

 如果你熟悉命令行的用法,你会发现我还没讲到这一选项的短版本。这也很简单:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="increase output verbosity",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

 效果就像这样:

$ python3 prog.py -v
verbosity turned on
$ python3 prog.py --help
usage: prog.py [-h] [-v]

options:
  -h, --help     show this help message and exit
  -v, --verbose  increase output verbosity

 可以注意到,这一新的能力也反映在帮助文本里。

1.6 结合位置参数和可选参数

 我们的程序变得越来越复杂了:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbose:
    print(f"the square of {args.square} equals {answer}")
else:
    print(answer)

 接着是输出:

$ python3 prog.py
usage: prog.py [-h] [-v] square
prog.py: error: the following arguments are required: square
$ python3 prog.py 4
16
$ python3 prog.py 4 --verbose
the square of 4 equals 16
$ python3 prog.py --verbose 4
the square of 4 equals 16
  • 我们带回了一个位置参数,结果发生了报错。

  • 注意顺序无关紧要。

 给我们的程序加上接受多个冗长度的值,然后实际来用用:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", type=int,
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity == 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

 和输出:

$ python3 prog.py 4
16
$ python3 prog.py 4 -v
usage: prog.py [-h] [-v VERBOSITY] square
prog.py: error: argument -v/--verbosity: expected one argument
$ python3 prog.py 4 -v 1
4^2 == 16
$ python3 prog.py 4 -v 2
the square of 4 equals 16
$ python3 prog.py 4 -v 3
16

 除了最后一个,看上去都不错。最后一个暴露了我们的程序中有一个 bug。我们可以通过限制 --verbosity 选项可以接受的值来修复它:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity == 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

 和输出:

$ python3 prog.py 4 -v 3
usage: prog.py [-h] [-v {0,1,2}] square
prog.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)
$ python3 prog.py 4 -h
usage: prog.py [-h] [-v {0,1,2}] square

positional arguments:
  square                display a square of a given number

options:
  -h, --help            show this help message and exit
  -v {0,1,2}, --verbosity {0,1,2}
                        increase output verbosity

 注意这一改变同时反应在错误信息和帮助信息里。

 现在,让我们使用另一种的方式来改变冗长度。这种方式更常见,也和 CPython 的可执行文件处理它自己的冗长度参数的方式一致(参考 python --help 的输出):

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display the square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity == 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

 我们引入了另一种动作 “count”,来统计特定选项出现的次数。

$ python3 prog.py 4
16
$ python3 prog.py 4 -v
4^2 == 16
$ python3 prog.py 4 -vv
the square of 4 equals 16
$ python3 prog.py 4 --verbosity --verbosity
the square of 4 equals 16
$ python3 prog.py 4 -v 1
usage: prog.py [-h] [-v] square
prog.py: error: unrecognized arguments: 1
$ python3 prog.py 4 -h
usage: prog.py [-h] [-v] square

positional arguments:
  square           display a square of a given number

options:
  -h, --help       show this help message and exit
  -v, --verbosity  increase output verbosity
$ python3 prog.py 4 -vvv
16
  • 是的,它现在比前一版本更像是一个标志(和 action="store_true" 相似)。这能解释它为什么报错。

  • 它也表现得与 “store_true” 的行为相似。

  • 这给出了一个关于 count 动作的效果的演示。你之前很可能应该已经看过这种用法。

  • 如果你不添加 -v 标志,这一标志的值会是 None

  • 如期望的那样,添加该标志的长形态能够获得相同的输出。

  • 可惜的是,对于我们的脚本获得的新能力,我们的帮助输出并没有提供很多信息,但我们总是可以通过改善文档来修复这一问题(比如通过 help 关键字参数)。

  • 最后一个输出暴露了我们程序中的一个 bug。

 让我们修复一下:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2

# bugfix: replace == with >=

if args.verbosity >= 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity >= 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

 这是它给我们的输出:

$ python3 prog.py 4 -vvv
the square of 4 equals 16
$ python3 prog.py 4 -vvvv
the square of 4 equals 16
$ python3 prog.py 4
Traceback (most recent call last):
  File "prog.py", line 11, in <module>
    if args.verbosity >= 2:
TypeError: '>=' not supported between instances of 'NoneType' and 'int'
  • 第一组输出很好,修复了之前的 bug。也就是说,我们希望任何 >= 2 的值尽可能详尽。

  • 第三组输出并不理想。

 让我们修复那个 bug:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", action="count", default=0,
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity >= 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity >= 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

 我们刚刚引入了又一个新的关键字 default。我们把它设置为 0 来让它可以与其他整数值相互比较。记住,默认情况下如果一个可选参数没有被指定,它的值会是 None,并且它不能和整数值相比较(所以产生了 TypeError 异常)。

 然后:

$ python3 prog.py 4
16

 凭借我们目前已学的东西你就可以做到许多事情,而我们还仅仅学了一些皮毛而已。 argparse 模块是非常强大的,在结束篇教程之前我们将再探索更多一些内容。

1.7 进行一些小小的改进

 如果我们想扩展我们的简短程序来执行其他幂次的运算,而不仅是乘方:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
answer = args.x**args.y
if args.verbosity >= 2:
    print(f"{args.x} to the power {args.y} equals {answer}")
elif args.verbosity >= 1:
    print(f"{args.x}^{args.y} == {answer}")
else:
    print(answer)

 输出:

$ python3 prog.py
usage: prog.py [-h] [-v] x y
prog.py: error: the following arguments are required: x, y
$ python3 prog.py -h
usage: prog.py [-h] [-v] x y

positional arguments:
  x                the base
  y                the exponent

options:
  -h, --help       show this help message and exit
  -v, --verbosity
$ python3 prog.py 4 2 -v
4^2 == 16

 请注意到目前为止我们一直在使用详细级别来 更改 所显示的文本。 以下示例则使用详细级别来显示 更多的 文本:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
answer = args.x**args.y
if args.verbosity >= 2:
    print(f"Running '{__file__}'")
if args.verbosity >= 1:
    print(f"{args.x}^{args.y} == ", end="")
print(answer)

输出:

$ python3 prog.py 4 2
16
$ python3 prog.py 4 2 -v
4^2 == 16
$ python3 prog.py 4 2 -vv
Running 'prog.py'
4^2 == 16
1.8 矛盾的选项

 到目前为止,我们一直在使用 argparse.ArgumentParser 实例的两个方法。 让我们再介绍第三个方法 add_mutually_exclusive_group()。 它允许我们指定彼此相互冲突的选项。 让我们再更改程序的其余部分以便使用新功能更有意义:我们将引入 --quiet 选项,它将与 --verbose 正好相反:

import argparse

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print(f"{args.x} to the power {args.y} equals {answer}")
else:
    print(f"{args.x}^{args.y} == {answer}")

 我们的程序现在变得更简洁了,我们出于演示需要略去了一些功能。 无论如何,输出是这样的:

$ python3 prog.py 4 2
4^2 == 16
$ python3 prog.py 4 2 -q
16
$ python3 prog.py 4 2 -v
4 to the power 2 equals 16
$ python3 prog.py 4 2 -vq
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
$ python3 prog.py 4 2 -v --quiet
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

 这应该很容易理解。 我添加了末尾的输出这样你就可以看到其所达到的灵活性,即混合使用长和短两种形式的选项。

 在我们收尾之前,你也许希望告诉你的用户这个程序的主要目标,以免他们还不清楚:

import argparse

parser = argparse.ArgumentParser(description="calculate X to the power of Y")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print("{} to the power {} equals {}".format(args.x, args.y, answer))
else:
    print("{}^{} == {}".format(args.x, args.y, answer))

 请注意用法文本中有细微的差异。 注意 [-v | -q],它的意思是说我们可以使用 -v-q,但不能同时使用两者:

$ python3 prog.py --help
usage: prog.py [-h] [-v | -q] x y

calculate X to the power of Y

positional arguments:
  x              the base
  y              the exponent

options:
  -h, --help     show this help message and exit
  -v, --verbose
  -q, --quiet

二、Click 模块

2.1 Click

Click 是一个以尽可能少的代码、以组合的方式创建优美命令行程序的 Python 包。是 Flask 的开发团队 Pallets 的另一款开源项目,它有很高的可配置性,同时也能开箱即用。

 它有如下三个特点:

  • 任意嵌套命令。

  • 自动生成帮助。

  • 支持运行时延迟加载子命令。

 Click 是一个第三方库,因此,在使用之前需要先安装:

pip install click
2.2 快速开始
2.2.1 业务逻辑

 首先定义业务逻辑,是不是感觉到有些难以置信呢?

 Argparse 业务逻辑是被放在最后一步,但 Click 却是放在第一步。细想想 Click 的这种方式才更符合人的思维吧?不论用什么命令行框架,我们最终关心的就是实现业务逻辑,其它的能省则省。

 我们以官方文档示例为例,来介绍 Click 的用法和哲学。假设命令行程序的输入是 name 和 count,功能是打印指定次数的名字。

 那么在 hello.py 中,很容易写出如下代码:

def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

 这段代码的逻辑很简单,就是循环 count 次,使用 click.echo 打印 name。其中,click.echoprint 的作用相似,但功能更加强大,能处理好 Unicode 和 二进制数据的情况。同时有更好的兼容性,因为 Python2 和 Python3 的用法有些差异。

2.2.2 定义参数

 很显然,我们需要针对 countname 来定义它们所对应的参数信息。

  • count 对应为命令行选项 --count,类型为数字,我们希望在不提供参数时,其默认值是 1。

  • name 对应为命令行选项 --name,类型为字符串,我们希望在不提供参数时,能给人提示。

 使用 click,就可以写成下面这样:

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')

def hello(count, name):
    ...

if __name__ == '__main__':
    hello()

 在上面的示例中:

  • 使用装饰器的方式,即定义了参数,又将之与处理逻辑绑定,这真是优雅。和 argparse 比起来,就少了一步绑定过程。

  • 使用 click.command 表示 hello 是对命令的处理。

  • 使用 click.option 来定义参数选项。

    • 对于 --count 来说,使用 default 来指定默认值。而由于默认值是数字,进而暗示 --count 选项的类型为数字。

    • 对于 --name 来说,使用 prompt 来指定未输入该选项时的提示语。

    • 使用 help 来指定帮助信息。

 不论是装饰器的方式、还是各种默认行为,click 都是像它的介绍所说的那样,让人尽可能少地编写代码,让整个过程变得快速而有趣。

 执行情况:

$ python hello.py
Your name: Ethan # 这里会显示 'Your name: '(对应代码中的 prompt),接受用户输入
Hello Ethan!

$ python hello.py --help   # click 帮我们自动生成了 `--help` 用法
Usage: hello.py [OPTIONS]

  Simple program that greets NAME for a total of COUNT times.

Options:
  --count INTEGER  Number of greetings.
  --name TEXT      The person to greet.
  --help           Show this message and exit.

$ python hello.py --count 3 --name Ethan    # 指定 count 和 name 的值
Hello Ethan!
Hello Ethan!
Hello Ethan!

$ python hello.py --count=3 --name=Ethan    # 也可以使用 `=`,和上面等价
Hello Ethan!
Hello Ethan!
Hello Ethan!

$ python hello.py --name=Ethan              # 没有指定 count,默认值是 1
Hello Ethan!
2.3 参数

 在概念上, click 把命令行分为 3 个组成:参数、选项和命令。

  • 参数 就是跟在命令后的除选项外的内容,比如 git add a.txt 中的 a.txt 就是表示文件路径的参数。

  • 选项 就是以 --- 开头的参数,比如 -f--file

  • 命令 就是命令行的初衷了,比如 git 就是命令,而 git add 中的 add 则是 git 的子命令。

2.3.1 基本参数

基本参数 就是通过位置里指定参数值。

 比如,我们可以指定两个位置参数 xy ,先添加的 x 位于第一个位置,后加入的 y 位于第二个位置。那么在命令行中输入 1 2的时候,分别对应到的就是 xy

@click.command()
@click.argument('x')
@click.argument('y')
def hello(x, y):
    print(x, y)
2.3.2 参数类型

参数类型 就是将参数值作为什么类型去解析,默认情况下是字符串类型。我们可以通过 type 入参来指定参数类型。

click 支持的参数类型多种多样:

  • str / click.STRING 表示字符串类型,这也是默认类型。

  • int / click.INT 表示整型。

  • float / click.FLOAT 表示浮点型。

  • bool / click.BOOL 表示布尔型。很棒之处在于,它会识别表示真/假的字符。对于 1yesytrue 会转化为 True0nonfalse 会转化为 False

  • click.UUID 表示 UUID,会自动将参数转换为 uuid.UUID 对象。

  • click.FILE 表示文件,会自动将参数转换为文件对象,并在命令行结束时自动关闭文件。

  • click.PATH 表示路径。

  • click.Choice 表示选择选项。

  • click.IntRange 表示范围选项。

 同 argparse 一样,click 也支持自定义类型,需要编写 click.ParamType 的子类,并重载 convert 方法。

 官网提供了一个例子,实现了一个整数类型,除了普通整数之外,还接受十六进制和八进制数字, 并将它们转换为常规整数:

class BasedIntParamType(click.ParamType):
    name = "integer"

    def convert(self, value, param, ctx):
        try:
            if value[:2].lower() == "0x":
                return int(value[2:], 16)
            elif value[:1] == "0":
                return int(value, 8)
            return int(value, 10)
        except TypeError:
            self.fail(
                "expected string for int() conversion, got "
                f"{value!r} of type {type(value).__name__}",
                param,
                ctx,
            )
        except ValueError:
            self.fail(f"{value!r} is not a valid integer", param, ctx)

BASED_INT = BasedIntParamType()
2.3.3 文件参数

 在基本参数的基础上,通过指定参数类型,我们就能构建出各类参数。

文件参数 是非常常用的一类参数,通过 type=click.File 指定,它能正确处理所有 Python 版本的 unicode 和 字节,使得处理文件十分方便。

@click.command()
@click.argument('input', type=click.File('rb'))  # 指定文件为二进制读
@click.argument('output', type=click.File('wb'))  # 指定文件为二进制写
def inout(input, output):
    whileTrue:
        chunk = input.read(1024)  # 此时 input 为文件对象,每次读入 1024 字节
        if not chunk:
            break
        output.write(chunk)  # 此时 output 为文件对象,写入上步读入的内容
2.3.4 文件路径参数

文件路径参数 用来处理文件路径,可以对路径做是否存在等检查,通过 type=click.Path 指定。不论文件名是 unicode 还是字节类型,获取到的参数类型都是 unicode 类型。

@click.command()
@click.argument('filename', type=click.Path(exists=True))  # 要求给定路径存在,否则报错
def hello(filename):
    click.echo(click.format_filename(filename))

 如果文件名是以 - 开头,会被误认为是命令行选项,这个时候需要在参数前加上 -- 和空格,比如:

$ python hello.py -- -foo.txt
-foo.txt
2.3.5 选择项参数

选择项参数 用来限定参数内容,通过 type=click.Choice 指定。

 比如,指定文件读取方式限制为 read-onlyread-write

@click.command()
@click.argument('mode', type=click.Choice(['read-only', 'read-write']))
def hello(mode):
    click.echo(mode)
2.3.6 可变参数

可变参数 用来定义一个参数可以有多个值,且能通过 nargs 来定义值的个数,取得的参数的变量类型为元组。

 若 nargs=NN为一个数字,则要求该参数提供 N 个值。若 N-1 则接受提供无数量限制的参数,如:

@click.command()
@click.argument('foo', nargs=-1)
@click.argument('bar', nargs=1)
def hello(foo, bar):
    pass

 如果要实现 argparse 中要求参数数量为 1 个或多个的功能,则指定 nargs=-1required=True 即可:

@click.command()
@click.argument('foo', nargs=-1, required=True)
def hello(foo, bar):
    pass
2.3.7 从环境变量读取参数

 通过在 click.argument 中指定 envvar,则可读取指定名称的环境变量作为参数值,比如:

@click.command()
@click.argument('filename', envvar='FILENAME')
def hello(filename):
    print(filename)

 执行如下命令查看效果:

$ FILENAME=hello.txt python3 hello.py
hello.txt

 而在 argparse 中,则需要自己从环境变量中读取。

3. 选项

 通过 click.option 可以给命令增加选项,并通过配置函数的参数来配置不同功能的选项。

3.1 给选项命名

click.option 中的命令规则可参考参数名称[2]。它接受的前两个参数为长、短选项(顺序随意),其中:

  • 长选项以 “–” 开头,比如 “–string-to-echo”。

  • 短选项以 “-” 开头,比如 “-s”。

 第三个参数为选项参数的名称,如果不指定,将会使用长选项的下划线形式名称:

@click.command()
@click.option('-s', '--string-to-echo')
def echo(string_to_echo):
    click.echo(string_to_echo)

 显示指定为 string:

@click.command()
@click.option('-s', '--string-to-echo', 'string')
def echo(string):
    click.echo(string)
3.3 基本值选项

 值选项是非常常用的选项,它接受一个值。如果在命令行中提供了值选项,则需要提供对应的值;反之则使用默认值。若没在 click.option 中指定默认值,则默认值为 None,且该选项的类型为 STRING[3];反之,则选项类型为默认值的类型。

 比如,提供默认值为 1,则选项类型为 INT[4]

@click.command()
@click.option('--n', default=1)
def dots(n):
    click.echo('.' * n)

 如果要求选项为必填,则可指定 click.optionrequired=True

@click.command()
@click.option('--n', required=True, type=int)
def dots(n):
    click.echo('.' * n)

 如果选项名称和 Python 中的关键字冲突,则可以显式的指定选项名称。比如将 --from 的名称设置为 from_

@click.command()
@click.option('--from', '-f', 'from_')
@click.option('--to', '-t')
def reserved_param_name(from_, to):
    click.echo(f'from {from_} to {to}')

 如果要在帮助中显式默认值,则可指定 click.optionshow_default=True

@click.command()
@click.option('--n', default=1, show_default=True)
def dots(n):
    click.echo('.' * n)

 在命令行中调用则有:

$ dots --help
Usage: dots [OPTIONS]

Options:
  --n INTEGER  [default: 1]
  --help       Show this message and exit.
3.4 多值选项

 有时,我们会希望命令行中一个选项能接收多个值,通过指定 click.option 中的 nargs 参数(必须是大于等于 0)。这样,接收的多值选项就会变成一个元组。

 比如,在下面的示例中,当通过 --pos 指定多个值时,pos 变量就是一个元组,里面的每个元素是一个 float

@click.command()
@click.option('--pos', nargs=2, type=float)
def findme(pos):
    click.echo(pos)

 在命令行中调用则有:

$ findme --pos 2.0 3.0
(1.0, 2.0)

 有时,通过同一选项指定的多个值得类型可能不同,这个时候可以指定 click.option 中的 type=(类型1, 类型2, ...) 来实现。而由于元组的长度同时表示了值的数量,所以就无须指定 nargs 参数。

@click.command()
@click.option('--item', type=(str, int))
def putitem(item):
    click.echo('name=%s id=%d' % item)

 在命令行中调用则有:

$ putitem --item peter 1338
name=peter id=1338
3.5 多选项

 不同于多值选项是通过一个选项指定多个值,多选项则是使用多个相同选项分别指定值,通过 click.option 中的 multiple=True 来实现。

 当我们定义如下多选项:

@click.command()
@click.option('--message', '-m', multiple=True)
def commit(message):
    click.echo('\n'.join(message))

便可以指定任意数量个选项来指定值,获取到的 message 是一个元组:

$ commit -m foo -m bar --message baz
foo
bar
baz
3.6 计值选项

 有时我们可能需要获得选项的数量,那么可以指定 click.option 中的 count=True 来实现。

 最常见的使用场景就是指定多个 --verbose-v 选项来表示输出内容的详细程度。

@click.command()
@click.option('-v', '--verbose', count=True)
def log(verbose):
    click.echo(f'Verbosity: {verbose}')

 在命令行中调用则有:

$ log -vvv
Verbosity: 3

 通过上面的例子,verbose 就是数字,表示 -v 选项的数量,由此可以进一步使用该值来控制日志的详细程度。

3.7 布尔选项

 布尔选项用来表示真或假,它有多种实现方式:

  • 通过 click.optionis_flag=True 参数来实现:
import sys

@click.command()
@click.option('--shout', is_flag=True)
def info(shout):
    rv = sys.platform
    if shout:
        rv = rv.upper() + '!!!!111'
    click.echo(rv)

 在命令行中调用则有:

$ info --shout
LINUX!!!!111
  • 通过在 click.option 的选项定义中使用 / 分隔表示真假两个选项来实现:
import sys

@click.command()
@click.option('--shout/--no-shout', default=False)
def info(shout):
    rv = sys.platform
    if shout:
        rv = rv.upper() + '!!!!111'
    click.echo(rv)

 在命令行中调用则有:

$ info --shout
LINUX!!!!111
$ info --no-shout
linux

 在 Windows 中,一个选项可以以 / 开头,这样就会真假选项的分隔符冲突了,这个时候可以使用 ; 进行分隔:

@click.command()
@click.option('/debug;/no-debug')
def log(debug):
    click.echo(f'debug={debug}')

if __name__ == '__main__':
    log()

 在 cmd 中调用则有:

> log /debug
debug=True
3.8 特性切换选项

 所谓特性切换就是切换同一个操作对象的不同特性,比如指定 --upper 就让输出大写,指定 --lower 就让输出小写。这么来看,布尔值其实是特性切换的一个特例。

 要实现特性切换选项,需要让多个选项都有相同的参数名称,并且定义它们的标记值 flag_value

import sys

@click.command()
@click.option('--upper', 'transformation', flag_value='upper',
              default=True)
@click.option('--lower', 'transformation', flag_value='lower')
def info(transformation):
    click.echo(getattr(sys.platform, transformation)())

 在命令行中调用则有:

$ info --upper
LINUX
$ info --lower
linux
$ info
LINUX

 在上面的示例中,--upper--lower 都有相同的参数值 transformation

  • 当指定 --upper 时,transformation 就是 --upper 选项的标记值 upper

  • 当指定 --lower 时,transformation 就是 --lower 选项的标记值 lower

 进而就可以做进一步的业务逻辑处理。

3.9 选择项选项

选择项选项 和 上篇文章中介绍的 选择项参数 类似,只不过是限定选项内容,依旧是通过 type=click.Choice 实现。此外,case_sensitive=False 还可以忽略选项内容的大小写。

@click.command()
@click.option('--hash-type',
              type=click.Choice(['MD5', 'SHA1'], case_sensitive=False))
def digest(hash_type):
    click.echo(hash_type)

 在命令行中调用则有:

$ digest --hash-type=MD5
MD5

$ digest --hash-type=md5
MD5

$ digest --hash-type=foo
Usage: digest [OPTIONS]
Try "digest --help"forhelp.

Error: Invalid value for"--hash-type": invalid choice: foo. (choose from MD5, SHA1)

$ digest --help
Usage: digest [OPTIONS]

Options:
  --hash-type [MD5|SHA1]
  --help                  Show this message and exit.
3.10 提示选项

 顾名思义,当提供了选项却没有提供对应的值时,会提示用户输入值。这种交互式的方式会让命令行变得更加友好。通过指定 click.option 中的 prompt 可以实现。

  • prompt=True 时,提示内容为选项的参数名称。
@click.command()
@click.option('--name', prompt=True)
def hello(name):
    click.echo(f'Hello {name}!')

 在命令行调用则有:

$ hello --name=John
Hello John!
$ hello
Name: John
Hello John!
  • prompt='Your name please' 时,提示内容为指定内容。
@click.command()
@click.option('--name', prompt='Your name please')
def hello(name):
    click.echo(f'Hello {name}!')

 在命令行中调用则有:

$ hello
Your name please: John
Hello John!

 基于提示选项,我们还可以指定 hide_input=True 来隐藏输入,confirmation_prompt=True 来让用户进行二次输入,这非常适合输入密码的场景。

@click.command()
@click.option('--password', prompt=True, hide_input=True,
              confirmation_prompt=True)
def encrypt(password):
    click.echo(f'Encrypting password to {password.encode("rot13")}')

 当然,也可以直接使用 click.password_option

@click.command()
@click.password_option()
def encrypt(password):
    click.echo(f'Encrypting password to {password.encode("rot13")}')

 我们还可以给提示选项设置默认值,通过 default 参数进行设置,如果被设置为函数,则可以实现动态默认值。

@click.command()
@click.option('--username', prompt=True,
              default=lambda: os.environ.get('USER', ''))
def hello(username):
    print("Hello,", username)

 详情请阅读 Dynamic Defaults for Prompts[5]

3.11 范围选项

 如果希望选项的值在某个范围内,就可以使用范围选项,通过指定 type=click.IntRange 来实现。它有两种模式:

  • 默认模式(非强制模式),如果值不在区间范围内将会引发一个错误。如 type=click.IntRange(0, 10) 表示范围是 [0, 10],超过该范围报错。

  • 强制模式,如果值不在区间范围内,将会强制选取一个区间临近值。如 click.IntRange(0, None, clamp=True) 表示范围是 [0, +∞),小于 0 则取 0,大于 20 则取 20。其中 None 表示没有限制。

@click.command()
@click.option('--count', type=click.IntRange(0, None, clamp=True))
@click.option('--digit', type=click.IntRange(0, 10))
def repeat(count, digit):
    click.echo(str(digit) * count)

if __name__ == '__main__':
    repeat()

 在命令行中调用则有:

$ repeat --count=1000 --digit=5
55555555555555555555
$ repeat --count=1000 --digit=12
Usage: repeat [OPTIONS]

Error: Invalid value for"--digit": 12 is not in the valid range of 0 to 10.
3.12 回调和优先

回调通过 click.option 中的 callback 可以指定选项的回调,它会在该选项被解析后调用。回调函数的签名如下:

def callback(ctx, param, value):
    pass

其中:

  • ctx 是命令的上下文 click.Context[6]

  • param 为选项变量 click.Option[7]

  • value 为选项的值。

使用回调函数可以完成额外的参数校验逻辑。比如,通过 --rolls 的选项来指定摇骰子的方式,内容为“{N}d{M}”,表示 M 面的骰子摇 N 次,N 和 M 都是数字。在真正的处理 rolls 前,我们需要通过回调函数来校验它的格式:

def validate_rolls(ctx, param, value):
    try:
        rolls, dice = map(int, value.split('d', 2))
        return (dice, rolls)
    except ValueError:
        raise click.BadParameter('rolls need to be in format NdM')

@click.command()
@click.option('--rolls', callback=validate_rolls, default='1d6')
def roll(rolls):
    click.echo('Rolling a %d-sided dice %d time(s)' % rolls)

这样,当我们输入错误格式时,变会校验不通过:

$ roll --rolls=42
Usage: roll [OPTIONS]

Error: Invalid value for"--rolls": rolls need to be in format NdM

输入正确格式时,则正常输出信息:

$ roll --rolls=2d12
Rolling a 12-sided dice 2 time(s)

优先通过 click.option 中的 is_eager 可以让该选项成为优先选项,这意味着它会先于所有选项处理。

利用回调和优先选项,我们就可以很好地实现 --version 选项。不论命令行中写了多少选项和参数,只要包含了 --version,我们就希望它打印版本就退出,而不执行其他选项的逻辑,那么就需要让它成为优先选项,并且在回调函数中打印版本。

 此外,在 click 中每个选项都对应到命令处理函数的同名参数,如果不想把该选项传递到处理函数中,则需要指定 expose_value=True,于是有:

def print_version(ctx, param, value):
    ifnot value or ctx.resilient_parsing:
        return
    click.echo('Version 1.0')
    ctx.exit()

@click.command()
@click.option('--version', is_flag=True, callback=print_version,
              expose_value=False, is_eager=True)
def hello():
    click.echo('Hello World!')

 当然 click 提供了便捷的 click.version_option 来实现 --version

@click.command()
@click.version_option(version='0.1.0')
def hello():
    pass
3.13 Yes 选项

 基于前面的学习,我们可以实现 Yes 选项,也就是对于某些操作,不提供 --yes 则进行二次确认,提供了则直接操作:

def abort_if_false(ctx, param, value):
    if not value:
        ctx.abort()

@click.command()
@click.option('--yes', is_flag=True, callback=abort_if_false,
              expose_value=False,
              prompt='Are you sure you want to drop the db?')
def dropdb():
    click.echo('Dropped all tables!')

 当然 click 提供了便捷的 click.confirmation_option 来实现 Yes 选项:

@click.command()
@click.confirmation_option(prompt='Are you sure you want to drop the db?')
def dropdb():
    click.echo('Dropped all tables!')

 在命令行中调用则有:

$ dropdb
Are you sure you want to drop the db? [y/N]: n
Aborted!
$ dropdb --yes
Dropped all tables!
3.14 其它增强功能

click 支持从环境中读取选项的值,这是 argparse 所不支持的,可参阅官方文档的 Values from Environment Variables[8] 和 Multiple Values from Environment Values[9]

click 支持指定选项前缀,你可以不使用 - 作为选项前缀,还可使用 +/,当然在一般情况下并不建议这么做。详情参阅官方文档的 Other Prefix Characters[10]

4.1 命令和组

Click 中非常重要的特性就是任意嵌套命令行工具的概念,通过 CommandGroup (实际上是 MultiCommand)来实现。

 所谓命令组就是若干个命令(或叫子命令)的集合,也成为多命令。

4.2 回调调用

 对于一个普通的命令来说,回调发生在命令被执行的时候。如果这个程序的实现中只有命令,那么回调总是会被触发,就像我们在上一篇文章中举出的所有示例一样。不过像 --help 这类选项则会阻止进入回调。

对于组和多个子命令来说,情况略有不同。回调通常发生在子命令被执行的时候:

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

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

 执行效果如下:

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

 在上面的示例中,我们将函数 cli 定义为一个组,把函数 sync 定义为这个组内的子命令。当我们调用 tool.py --debug sync 命令时,会依次触发 clisync 的处理逻辑(也就是命令的回调)。

4.3 嵌套处理和上下文

 从上面的例子可以看到,命令组 cli 接收的参数和子命令 sync 彼此独立。但是有时我们希望在子命令中能获取到命令组的参数,这就可以用 Context 来实现。

 每当命令被调用时,click 会创建新的上下文,并链接到父上下文。通常,我们是看不到上下文信息的。但我们可以通过 pass_context 装饰器来显式让 click 传递上下文,此变量会作为第一个参数进行传递。

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    # 确保 ctx.obj 存在并且是个 dict。 (以防 `cli()` 指定 obj 为其他类型
    ctx.ensure_object(dict)

    ctx.obj['DEBUG'] = debug

@cli.command()
@click.pass_context
def sync(ctx):
    click.echo('Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off'))

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

 在上面的示例中:

  • 通过为命令组 cli 和子命令 sync 指定装饰器 click.pass_context,两个函数的第一个参数都是 ctx 上下文。

  • 在命令组 cli 中,给上下文的 obj 变量(字典)赋值。

  • 在子命令 sync 中通过 ctx.obj['DEBUG'] 获得上一步的参数。

  • 通过这种方式完成了从命令组到子命令的参数传递。

4.4 不使用命令来调用命令组

 默认情况下,调用子命令的时候才会调用命令组。而有时你可能想直接调用命令组,通过指定 click.groupinvoke_without_command=True 来实现:

@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('I am about to invoke %s' % 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

 在上面的示例中,通过 ctx.invoked_subcommand 来判断是否由子命令触发,针对两种情况打印日志。

4.5 自定义命令组/多命令

 除了使用 click.group 来定义命令组外,你还可以自定义命令组(也就是多命令),这样你就可以延迟加载子命令,这会很有用。

 自定义多命令需要实现 list_commandsget_command 方法:

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'):
                rv.append(filename[:-3])
        rv.sort()
        return rv

    def get_command(self, ctx, name):
        ns = {}
        fn = os.path.join(plugin_folder, name + '.py') # 命令对应的 Python 文件
        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.')

# 等价方式是通过 click.command 装饰器,指定 cls=MyCLI
# @click.command(cls=MyCLI)
# def cli():
#     pass

if __name__ == '__main__':
    cli()
4.6 合并命令组/多命令

 当有多个命令组,每个命令组中有一些命令,你想把所有的命令合并在一个集合中时,click.CommandCollection 就派上了用场:

@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

 从上面的示例可以看出,cmd1cmd2 分别属于 cli1cli2,通过 click.CommandCollection 可以将这些子命令合并在一起,将其能力提供个同一个命令程序。

Tips:如果多个命令组中定义了同样的子命令,那么取第一个命令组中的子命令。

4.7 链式命令组/多命令

 有时单级子命令可能满足不了你的需求,你甚至希望能有多级子命令。典型地,setuptools 包中就支持多级/链式子命令: setup.py sdist bdist_wheel upload。在 click 3.0 之后,实现链式命令组变得非常简单,只需在 click.group 中指定 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
4.8 命令组/多命令管道

 链式命令组中一个常见的场景就是实现管道,这样在上一个命令处理好后,可将结果传给下一个命令处理。

 实现命令组管道的要点是让每个命令返回一个处理函数,然后编写一个总的管道调度函数(并由 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

 在上面的示例中:

  • cli 定义为了链式命令组,并且指定 invoke_without_command=True,也就意味着可以不传子命令来触发命令组
  • 定义了三个命令处理函数,分别对应 uppercaselowercasestrip 命令
  • 在管道调度函数 process_pipeline 中,将输入 input 变成生成器,然后调用处理函数(实际输入几个命令,就有几个处理函数)进行处理
4.9 覆盖默认值

 默认情况下,参数的默认值是从通过装饰器参数 default 定义。我们还可以通过 Context.default_map 上下文字典来覆盖默认值:

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

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

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

 在上面的示例中,通过在 cli 中指定 default_map 变可覆盖命令(一级键)的选项(二级键)默认值(二级键的值)。

 我们还可以在 click.group 中指定 context_settings 来达到同样的目的:

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('Serving on http://127.0.0.1:%d/' % port)

if __name__ == '__main__':
    cli()

 调用命令则有:

$ cli runserver
Serving on http://127.0.0.1:5000/
5.1 增强功能

 这部分是 click 锦上添花的功能,以帮助我们更加轻松地打造一个更加强大的命令行程序。

5.2 Bash 补全

 Bash 补全是 click 提供的一个非常便捷和强大的功能,这是它比 argpase 强大的一个表现。

 在命令行程序正确安装后,Bash 补全才可以使用。而如何安装可以参考 setup 集成。Click 目前仅支持 Bash 和 Zsh 的补全。

5.2.1 补全功能

 通常来说,Bash 补全支持对子命令、选项、以及选项或参数值得补全。比如:

$ repo <TAB><TAB>
clone    commit   copy     delete   setuser
$ repo clone -<TAB><TAB>
--deep     --help     --rev      --shallow  -r

 此外,click 还支持自定义补全,这在动态生成补全场景中很有用,使用 autocompletion 参数。autocompletion 需要指定为一个回调函数,并且返回字符串的列表。此函数接受三个参数:

  • ctx —— 当前的 click 上下文。

  • args 传入的参数列表。

  • incomplete 正在补全的词。

 这里有一个根据环境变量动态生成补全的示例:

import os

def get_env_vars(ctx, args, incomplete):
    return [k for k in os.environ.keys() if incomplete in k]

@click.command()
@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
def cmd1(envvar):
    click.echo('Environment variable: %s' % envvar)
    click.echo('Value: %s' % os.environ[envvar])

 在 ZSH 中,还支持补全帮助信息。只需将 autocompletion 回调函数中返回的字符串列表中的字符串改为二元元组,第一个元素是补全内容,第二个元素是帮助信息。

 这里有一个颜色补全的示例:

import os

def get_colors(ctx, args, incomplete):
    colors = [('red', 'help string for the clor red'),
              ('blue', 'help string for the color blue'),
              ('green', 'help string for the color green')]
    return [c for c in colors if incomplete in c[0]]

@click.command()
@click.argument("color", type=click.STRING, autocompletion=get_colors)
def cmd1(color):
    click.echo('Chosen color is %s' % color)
5.2.2 激活补全

 要激活 Bash 的补全功能,就需要告诉它你的命令行程序有补全的能力。通常通过一个神奇的环境变量 _<PROG_NAME>_COMPLETE 来告知,其中 <PROG_NAME> 是大写下划线形式的程序名称。

 比如有一个命令行程序叫做 foo-bar,那么对应的环境变量名称为 _FOO_BAR_COMPLETE,然后在 .bashrc 中使用 source 导出即可:

eval "$(_FOO_BAR_COMPLETE=source foo-bar)"

 或者在 .zshrc 中使用:

eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)"

 不过上面的方式总是在命令行程序启动时调用,这可能在有多个程序时减慢 shell 激活的速度。另一种方式是把命令放在文件中,就像这样:

# 针对 Bash
_FOO_BAR_COMPLETE=source foo-bar > foo-bar-complete.sh

# 针对 ZSH
_FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh

 然后把脚本文件路径加到 .bashrc.zshrc 中:

. /path/to/foo-bar-complete.sh
5.3 实用工具
5.3.1 打印到标准输出

echo() 函数可以说是最有用的实用工具了。它和 Python 的 print 类似,主要的区别在于它同时在 Python 2 和 3 中生效,能够智能地检测未配置正确的输出流,且几乎不会失败(除了 Python 3 中的少数限制)。

echo 即支持 unicode,也支持二级制数据,如:

import click

click.echo('Hello World!')

click.echo(b'\xe2\x98\x83', nl=False) # nl=False 表示不输出换行符
5.3.2 ANSI 颜色

 有些时候你可能希望输出是有颜色的,这尤其在输出错误信息时有用,而 click 在这方面支持的很好。

首先,你需要安装 colorama

pip install colorama

 然后,就可以使用 style() 函数来指定颜色:

import click

click.echo(click.style('Hello World!', fg='green'))
click.echo(click.style('Some more text', bg='blue', fg='white'))
click.echo(click.style('ATTENTION', blink=True, bold=True))

click 还提供了更加简便的函数 secho,它就是 echostyle 的组合:

click.secho('Hello World!', fg='green')
click.secho('Some more text', bg='blue', fg='white')
click.secho('ATTENTION', blink=True, bold=True)
5.3.3 分页支持

 有些时候,命令行程序会输出长文本,但你希望能让用户盘也浏览。使用 echo_via_pager() 函数就可以轻松做到。

 例如:

def less():
    click.echo_via_pager('\n'.join('Line %d' % idx
                                   for idx in range(200)))

 如果输出的文本特别大,处于性能的考虑,希望翻页时生成对应内容,那么就可以使用生成器:

def _generate_output():
    for idx in range(50000):
        yield "Line %d\n" % idx

@click.command()
def less():
    click.echo_via_pager(_generate_output())
5.3.4 清除屏幕

 使用 clear() 可以轻松清除屏幕内容:

import click
click.clear()
5.3.5 从终端获取字符

 通常情况下,使用内建函数 inputraw_input 获得的输入是用户输出一段字符然后回车得到的。但在有些场景下,你可能想在用户输入单个字符时就能获取到并且做一定的处理,这个时候 getchar() 就派上了用场。

 比如,根据输入的 yn 做特定处理:

import click

click.echo('Continue? [yn] ', nl=False)
c = click.getchar()
click.echo()
if c == 'y':
    click.echo('We will go on')
elif c == 'n':
    click.echo('Abort!')
else:
    click.echo('Invalid input :(')
5.3.6 等待按键

 在 Windows 的 cmd 中我们经常看到当执行完一个命令后,提示按下任意键退出。通过使用 pause() 可以实现暂停直至用户按下任意键:

import click
click.pause()
5.3.7 启动编辑器

 通过 edit() 可以自动启动编辑器。这在需要用户输入多行内容时十分有用。

 在下面的示例中,会启动默认的文本编辑器,并在里面输入一段话:

import click

def get_commit_message():
    MARKER = '# Everything below is ignored\n'
    message = click.edit('\n\n' + MARKER)
    if message is not None:
        return message.split(MARKER, 1)[0].rstrip('\n')

edit() 函数还支持打开特定文件,比如:

import click
click.edit(filename='/etc/passwd') 
5.3.8 启动应用程序

 通过 launch 可以打开 URL 或文件类型所关联的默认应用程序。如果设置 locate=True,则可以启动文件管理器并自动选中特定文件。

 示例:

# 打开浏览器,访问 URL
click.launch(https://click.palletsprojects.com/")

# 使用默认应用程序打开 txt 文件
click.launch("/my/downloaded/file.txt")

# 打开文件管理器,并自动选中 file.txt
click.launch("/my/downloaded/file.txt", locate=True)
5.3.9 显示进度条

click 内置了 progressbar() 函数来方便地显示进度条。

 它的用法也很简单,假定你有一个要处理的可迭代对象,处理完每一项就要输出一下进度,那么就有两种用法。

 用法一:使用 progressbar 构造出 bar 对象,迭代 bar 对象来自动告知进度:

import time
import click

all_the_users_to_process = ['a', 'b', 'c']

def modify_the_user(user):
    time.sleep(0.5)

with click.progressbar(all_the_users_to_process) as bar:
    for user in bar:
        modify_the_user(user)

 用法二:使用 progressbar 构造出 bar 对象,迭代原始可迭代对象,并不断向 bar 更新进度:

import time
import click

all_the_users_to_process = ['a', 'b', 'c']

def modify_the_user(user):
    time.sleep(0.5)

with click.progressbar(all_the_users_to_process) as bar:
    for user in enumerate(all_the_users_to_process):
        modify_the_user(user)
        bar.update(1)
5.3.10 更多实用工具

后记

除了这里显示的内容,argparseClick 模块还提供了更多功能。 它的文档相当详细和完整,包含大量示例。 完成这个教程之后,你应该能毫不困难地阅读该文档。

参考资料

[1]HelloGitHub-Team 仓库: https://github.com/HelloGitHub-Team/Article

[2]参数名称: https://click.palletsprojects.com/en/7.x/parameters/#parameter-names

[3]STRING: https://click.palletsprojects.com/en/7.x/api/#click.STRING

[4]INT: https://click.palletsprojects.com/en/7.x/api/#click.INT

[5]Dynamic Defaults for Prompts: https://click.palletsprojects.com/en/7.x/options/#dynamic-defaults-for-prompts

[6]click.Context: https://click.palletsprojects.com/en/7.x/api/#click.Context

[7]click.Option: https://click.palletsprojects.com/en/7.x/api/#click.Option

[8]Values from Environment Variables: https://click.palletsprojects.com/en/7.x/options/#values-from-environment-variables

[9]Multiple Values from Environment Values: https://click.palletsprojects.com/en/7.x/options/#multiple-values-from-environment-values

[10]Other Prefix Characters: https://click.palletsprojects.com/en/7.x/options/#other-prefix-characters

;