Click 如何调用 Command 函数并取得返回值

在项目的演进过程中,都会经历项目需求从少至多,由简单到复杂的过程。在这个不停追加的过程中,项目结构也会从一个小脚本就能搞定变化到需要由几个小脚本配合共同支撑,项目的代码也会从精简干练慢慢变到冗余、重合度高。

这时候,为了让项目结构更加稳健,易用性更高,可读性更好,是需要重建程序入口,把脚本工程化的。

看起来很复杂,实际上就是在项目演进的不同阶段适时地重构,每个优秀的项目都是经过千(多)锤(次)百(重)炼(构)才能形成的。

今天就从程序的入口开始,记录一次改造。(中间省略 1000 字为重构的细节及方法…)

在最初的版本,程序入口直接使用了简单粗暴的 if - else,带来的问题就是,无论如何封装抽象,都无法改变可读性极差的结果。于是在无力演进的情况下,选择一款 CLI 构建工具,提高可持续性,防止自己重造车轮。

CLI 构建工具 Click

Click 的基础使用方法和高级特性都可以从官方文档获取

以下谨记录个人使用到的,以及我认为最常用的操作。

基本用法

夏天来了,案例使用了冰淇淋,这样看起来会清凉一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import click

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

@click.command()
@click.option('--brand', default=1, prompt='请选择品牌:1.和路雪 2.雀巢 3.五羊 4.其它', help='小卖部品牌分类:1.和路雪 2.雀巢 3.五羊 4.其它')
@click.option('--category', default=1, prompt='请选择类别:1.雪糕 2.冰棍 3.杯装', help='冰淇淋类别:1.雪糕 2.冰棍 3.杯装')
def ice_cream_selection(brand, category):
"""冰淇淋订购程序"""
print(brand, category)
if category == 3:
result = taste_selection()
print(result)
print("finished!")

@click.command()
@click.option('--selected', default=0, prompt='请选择口味:1.香草味 2.巧克力味')
def taste_selection(selected):
return click.echo(selected)

test_group.add_command(ice_cream_selection)
test_group.add_command(taste_selection)

if __name__ == '__main__':
ice_cream_selection()
#test_group()

从上面的案例简单讲解使用方法

@click.command()

将函数注册为 Click 识别的命令

@click.option

option 为高度定制该命令的模块

  • '--brand' 此处声明变量,将会接收来自输入的数据
  • default 给予变量默认值
  • prompt 若没有在启动程序时传入该变量,则会在后续作为输入的提示
  • help 在 –help 时输出的帮助文本

让我们运行一下上面的程序试试

1
2
3
4
5
6
ubuntu@ubuntu:~/project/click-test$ python3 test.py 
请选择品牌:1.和路雪 2.雀巢 3.五羊 4.其它 [1]:
请选择类别:1.雪糕 2.冰棍 3.杯装 [1]: 3
1 3
请选择口味:1.香草味 2.巧克力味 [0]: 1
1

执行的路径和预期中一样,因为没有传入变量参数,所以需要按照预先设定好的 prompt 的提示填充变量。

再看看帮助功能

1
2
3
4
5
6
7
8
9
ubuntu@ubuntu:~/project/click-test$ python3 test.py --help
Usage: test.py [OPTIONS]

冰淇淋订购程序

Options:
--brand INTEGER 小卖部品牌分类:1.和路雪 2.雀巢 3.五羊 4.其它
--category INTEGER 冰淇淋类别:1.雪糕 2.冰棍 3.杯装
--help Show this message and exit.

建立组并集合子模块

为了方便演示,command 写在同一段伪代码里。

更好的使用方式是,将不同的 command 分布在不同的项目层次、不同的文件夹中,再使用 import 引入。

1
2
3
4
5
6
7
8
9
10
11
12
# test_group.py
@click.group()
def test_group():
pass

# 此处省略 command 的定义

test_group.add_command(ice_cream_selection)
test_group.add_command(taste_selection)

if __name__ == '__main__':
test_group()

click.group 链接子模块就是这么简单,但是运行的方式会有些不同

1
2
3
4
5
# 如果你编写 setup.py 并已安装
$ test_group ice_cream_selection --xxx xx --yyy yy

# 或者你还在调试
$ python3 test_group.py ice_cream_selection --xxx xx --yyy yy

模块调用模块后如何获取返回值

这是困扰了很久的问题,在程序执行总是会经历需要多重判断,获取一级类目下细分类目的情况,所以返回细分类目给上层倒是个需要的功能。

然而 Click 并不支持直接返回参数,就像下面这个案例一样,调用了 taste_selection 后,是无法取得选中的口味的!

1
2
3
4
@click.command()
@click.option('--selected', default=0, prompt='请选择口味:1.香草味 2.巧克力味')
def taste_selection(selected):
return selected

经过一番苦痛折磨,甚至尝试引入上下文 context 的方式,依旧无法获取返回值。

直到看到 Stackoverflow 上的这个回答:How do I return a value when @click.option is used to pass a command line argument to a function?

该回答下的另一个答主 abarnert 则给出了为何取不到 Click 返回值的解释:

click is trying to make your function into a good command-line citizen, so when you exit your function, it calls sys.exit with the appropriate number (0 if you return, 1 if you raise, and 2 if it failed to parse your arguments).

What programs usually do when they need to “return” text is to print it to standard output, which is exactly what click.echo is for.

因此,上面的返回只需要用 Click 的标准输出函数封装,即可取得相应的值。

  • click.echo(selected)
1
2
3
4
@click.command()
@click.option('--selected', default=0, prompt='请选择口味:1.香草味 2.巧克力味')
def taste_selection(selected):
return click.echo(selected)