Python Click 教程:从函数到专业命令行工具
Click 是 Pallets 生态下的 Python 命令行框架。它真正解决的痛点,是命令行工具一旦从"临时脚本"变成"要给别人反复使用的工具",外围代码会迅速失控:每个参数都要解析,每个选项都要校验类型和默认值,输错命令时要给出可读提示,还要维护帮助页、子命令结构、测试方式和最终发布入口。
如果不用 Click,一个看起来很简单的需求,比如支持 --count 3 Alice,你可能会先从 sys.argv 拿原始字符串:
python
import sys
args = sys.argv[1:]
接下来就要自己处理一连串细节:参数少了怎么办,--count 写错或漏值怎么办,"3" 怎么安全转成整数,用户输入 --help 时展示什么,错误信息要不要带用法说明。随着选项和子命令增加,这些判断会散落在业务函数周围,让真正的业务逻辑被解析、校验和提示代码包围。Click 的思路是:让函数专注业务,让装饰器描述命令行接口。
学习 Click,可以抓住这条主线:
text
函数 -> Command
函数参数 -> 命令行参数
多个 Command -> Group
一次命令调用 -> Context
正式安装运行 -> Entry Point
自动化测试 -> CliRunner
1. 第一个命令:一个函数如何变成 CLI
先看一个完整例子:
python
import click
@click.command()
@click.option("--count", default=1, help="重复次数")
@click.argument("name")
def hello(count: int, name: str) -> None:
"""向 NAME 打招呼。"""
for _ in range(count):
click.echo(f"Hello {name}!")
if __name__ == "__main__":
hello()
运行:
bash
python hello.py --count 3 Alice
输出:
text
Hello Alice!
Hello Alice!
Hello Alice!
这段代码里最重要的是三层装饰器。先不要急着逐个拆开,先把它们连起来看。代码表面上是从上往下写的:
python
@click.command()
@click.option("--count", default=1, help="重复次数")
@click.argument("name")
def hello(count: int, name: str) -> None:
...
但装饰器真正套到函数上时,是从下往上的。上面的写法大致等价于:
python
def hello(count: int, name: str) -> None:
...
hello = click.argument("name")(hello)
hello = click.option("--count", default=1, help="重复次数")(hello)
hello = click.command()(hello)
也就是说,最靠近函数的 @click.argument("name") 先处理 hello,把 name 这个位置参数记录到函数上;然后 @click.option(...) 再把 --count 这个命名选项记录到函数上;最后,最上面的 @click.command() 才把已经带有参数声明的函数包装成一个真正的 Click 命令对象。
这里容易混淆的是两个顺序:装饰器表达式本身的求值顺序是从上往下,也就是先求出 click.command(),再求出 click.option(...),最后求出 click.argument(...)。
但这些装饰器真正应用到 hello 函数上时,是从下往上。理解这个细节后,再看多层装饰器就不会混乱:靠近函数的装饰器先处理函数,最上面的 @click.command() 最后完成命令对象的创建。
有了这个整体模型,再拆开看每一层的作用就更清楚了。
python
@click.argument("name")
它声明了一个位置参数。用户调用:
bash
python hello.py --count 3 Alice
其中 Alice 就是 name。位置参数没有 --name 这种前缀,靠位置识别。默认情况下,argument 是必填的。如果少传 Alice,Click 会提示缺少 NAME。
python
@click.option("--count", default=1, help="重复次数")
它声明了一个命名选项。用户可以这样传:
bash
--count 3
这里的 --count 是命令行里的名字,Click 会把它转换成函数参数 count。default=1 表示用户不传时默认是 1。help="重复次数" 会出现在 --help 页面里。
还有一个细节:这个例子没有写 type=int,但因为 default=1 是整数,Click 会推断 count 是整数选项。所以:
bash
python hello.py --count abc Alice
会在进入业务函数之前报错。
python
@click.command()
它把 hello 函数转换成一个 Click Command 对象。也就是说,被装饰之后,模块里的 hello 不再只是普通 Python 函数,而是一个"可从命令行调用的命令对象"。这个命令对象会负责读取命令行输入、解析参数、处理 --help、校验类型,最后再调用内部保存的回调函数。
从最小结构上看,可以这样理解:在这一节的单命令写法里,@click.command() 是必须的,它负责完成"普通函数 -> 命令行命令"的转换;@click.argument(...) 和 @click.option(...) 是按需添加的参数声明。也就是说,如果一个命令不需要任何命令行输入,只写 @click.command() 也可以:
python
@click.command()
def version() -> None:
click.echo("1.0.0")
这个命令没有位置参数,也没有命名选项,但它仍然是一个合法的 Click 命令,可以运行,也会自动拥有 --help。
不过,对当前这个 hello 例子来说,@click.argument("name") 和 @click.option("--count", ...) 不能随便省。因为函数签名需要 count 和 name,命令行接口也希望支持:
bash
python hello.py --count 3 Alice
如果只保留 @click.command(),却仍然写成:
python
@click.command()
def hello(count: int, name: str) -> None:
...
Click 会创建一个"不接收任何参数"的命令。它不知道应该从命令行里读取 count 和 name,也就无法正确调用这个函数。
还要注意,"可选"这个词在这里有两层意思。@click.argument(...) 和 @click.option(...) 作为装饰器是按需使用的;但它们声明出来的命令行参数是否必填,是另一回事。默认情况下,argument 通常是必填的位置参数,而 option 通常是可以省略的命名选项,除非你给 option 设置 required=True。
最后看函数签名:
python
def hello(count: int, name: str) -> None:
这里的 count 和 name 不是你从 sys.argv 里手动取出来的,而是 Click 根据前面的装饰器解析后传入的。可以把它理解成:命令行里的 --count 3 Alice,最后会被 Click 整理成一次普通的 Python 函数调用:
python
hello(count=3, name="Alice")
这里有两个容易混淆的点。
第一,函数参数名要和 Click 生成的参数名对得上。@click.option("--count", ...) 默认会生成名为 count 的函数参数,@click.argument("name") 会生成名为 name 的函数参数,所以函数签名里写的是 count 和 name。如果你把函数写成 def hello(times: int, name: str) -> None,但装饰器仍然叫 --count,Click 仍会尝试传入 count=...,函数就接不上这个值。
第二,Python 类型注解不会自动驱动 Click 解析参数 。count: int 主要是给读者、编辑器和类型检查器看的;Click 不会因为你写了 count: int,就自动把命令行字符串转成整数。Click 的转换规则来自装饰器参数,例如:
python
@click.option("--count", type=int, default=1)
或者像这个例子一样,虽然没有显式写 type=int,但 default=1 是整数,Click 会据此推断 count 应该按整数处理。更推荐在教程和正式项目里显式写出 type=int,这样读代码的人不用猜类型来源。
2. 从命令行输入到函数调用:值是怎么流动的
当用户输入:
bash
python hello.py --count 3 Alice
Click 大致做了这些事:
text
读取命令行参数
-> 识别 --count 是 option
-> 识别 Alice 是 argument
-> 把 "3" 转成整数 3
-> 检查 name 是否存在
-> 调用内部回调函数,传入 count=3, name="Alice"
所以你可以把它理解成:命令行字符串最终被整理成一次干净的 Python 函数调用。
如果用户输入:
bash
python hello.py --count abc Alice
Click 会报类型错误,因为 abc 不能转换成整数。
如果用户输入:
bash
python hello.py --count 3
Click 会报缺少 NAME,因为 name 是必填位置参数。
这就是 Click 的重要价值:错误尽量发生在命令入口,而不是业务代码深处。
3. 为什么用 click.echo,而不是 print
示例里用了:
python
click.echo(f"Hello {name}!")
普通情况下,print() 也能工作。但官方示例倾向使用 click.echo(),因为它更适合 CLI:
python
click.echo("hello")
click.echo("error", err=True)
click.echo(click.style("success", fg="green"))
它能更稳健地处理 Unicode、stderr、颜色输出、Windows 控制台,以及输出重定向到文件时的样式处理。
经验规则很简单:
text
普通临时脚本:print 可以
正式命令行工具:优先 click.echo
4. Argument 和 Option:设计命令接口的第一原则
Click 的参数统称为 Parameter,分成两类:
text
Argument:位置参数
Option:命名选项
看这个命令:
bash
resize image.png --width 800 --format webp
image.png 是被处理的对象,所以适合做 argument。
--width 800 和 --format webp 是处理方式,所以适合做 option。
可以这样判断:
text
没有它,命令不知道处理什么:argument
没有它,命令仍能运行,只是采用默认行为:option
推荐设计:
python
@click.argument("source")
@click.option("-o", "--output")
@click.option("--format", type=click.Choice(["png", "webp", "jpg"]), default="png")
def convert(source, output, format):
...
对应:
bash
convert input.png -o output.webp --format webp
不推荐这样设计:
bash
convert input.png output.webp webp true 3
因为太多位置参数会让用户很难记住每个值的含义。
5. Option 的常见形态
option 通常用来描述"命令怎么执行",比如次数、模式、开关、配置来源。它的形态很多,但可以先按用途理解:
text
普通值 --name Alice
默认值 --count 3,不传时用默认值
显式类型 --count 3,把字符串转成 int
布尔开关 --verbose,出现就是 True
成对开关 --cache / --no-cache,明确开启或关闭
计数开关 -v、-vv、-vvv,用出现次数表示等级
重复选项 -m title -m body,多次传入同一个选项
多值选项 --pos 12.5 30.0,一次选项接收多个值
环境变量 从 APP_TOKEN 读取默认值
下面逐个看。
5.1 普通 option:接收一个字符串
最普通的 option 只声明一个命名选项。用户传什么,函数就收到什么:
python
@click.option("--name")
def hello(name):
...
调用:
bash
tool --name Alice
如果用户不传,name 默认是 None。
这种写法适合真正可以省略的配置项。如果业务逻辑不能接受 None,就应该提供默认值,或者设置为必填。
5.2 带默认值:用户不传时也有确定值
default 用来指定选项的默认值。用户不传这个 option 时,Click 会把默认值传给函数:
python
@click.option("--count", default=1)
def hello(count):
...
调用:
bash
tool
函数收到:
python
count == 1
有默认值之后,业务代码通常会更干净,因为函数内部不用先判断 count is None。
5.3 显式类型:让转换规则写在入口处
命令行里输入的内容本质上都是字符串。type 用来告诉 Click 应该把字符串转换成什么 Python 类型:
python
@click.option("--count", type=int, default=1)
这样用户输入:
bash
tool --count 3
函数收到的是整数 3,不是字符串 "3"。如果用户输入 --count abc,Click 会在进入函数之前报错。这比只依赖默认值推断更清晰,尤其适合教程和团队代码。
5.4 布尔开关:出现就是 True
如果一个选项不需要额外的值,只表示"是否启用某个行为",可以用 is_flag=True:
python
@click.option("--verbose", is_flag=True)
def cli(verbose):
...
调用结果:
bash
tool # verbose = False
tool --verbose # verbose = True
这种写法适合 --verbose、--debug、--dry-run 这类开关。用户只需要写出选项名,不需要再写 true 或 false。
5.5 成对布尔开关:明确开启或关闭
如果一个功能有默认状态,但你希望用户能明确打开或关闭,可以把两个选项写成一组:
python
@click.option("--cache/--no-cache", default=True)
def cli(cache):
...
调用结果:
bash
tool # cache = True
tool --no-cache # cache = False
这种写法适合默认开启但允许关闭的功能,例如 --color/--no-color、--metadata/--no-metadata。
5.6 计数 option:用出现次数表示等级
有些选项不是简单的开或关,而是可以叠加等级。典型例子是日志详细程度:
python
@click.option("-v", "--verbose", count=True)
def cli(verbose):
...
调用结果:
bash
tool # verbose = 0
tool -v # verbose = 1
tool -vvv # verbose = 3
这样用户不需要写 --verbose 3,而是使用命令行工具里更常见的 -v、-vv、-vvv。
5.7 重复 option:同一个选项传多次
multiple=True 表示同一个 option 可以出现多次。Click 会把所有值收集成一个元组:
python
@click.option("-m", "--message", multiple=True)
def commit(message):
...
调用:
bash
tool -m "title" -m "body"
函数收到:
python
message == ("title", "body")
这种写法适合标签、消息片段、多个过滤条件这类"数量不固定"的输入。
5.8 多值 option:一个选项后面接多个值
nargs 表示这个 option 一次要接收几个值。比如坐标通常成对出现:
python
@click.option("--pos", nargs=2, type=float)
def locate(pos):
...
调用:
bash
tool --pos 12.5 30.0
函数收到:
python
pos == (12.5, 30.0)
这里 type=float 会作用到两个值上,所以函数收到的是两个浮点数构成的元组。
5.9 环境变量:从外部环境读取值
有些值不适合每次都写在命令行里,比如 token、密钥、服务地址。可以用 envvar 让 Click 从环境变量读取:
python
@click.option("--token", envvar="APP_TOKEN")
def cli(token):
...
用户既可以传:
bash
tool --token xxx
也可以用环境变量提供:
bash
APP_TOKEN=xxx tool
如果命令行里显式传了 --token,通常应该优先使用命令行参数;没有传时,再从环境变量读取。这样既方便自动化脚本,也避免把敏感信息直接写进命令历史。
6. 类型系统:让错误停在入口处
命令行输入本质上都是字符串。Click 的 type 用来把字符串转换成业务代码真正需要的 Python 对象。
常见写法:
python
@click.option("--count", type=int)
@click.option("--ratio", type=float)
@click.option("--mode", type=click.Choice(["fast", "safe"]))
@click.option("--level", type=click.IntRange(0, 10))
def run(count, ratio, mode, level):
...
如果用户输入:
bash
tool --count abc
Click 会直接报错,不会进入 run()。
路径参数很常见,推荐这样写:
python
from pathlib import Path
@click.argument("source", type=click.Path(exists=True, path_type=Path))
def clean(source: Path):
click.echo(source.name)
这里:
python
exists=True
表示路径必须存在。
python
path_type=Path
表示函数收到的是 pathlib.Path 对象,而不是字符串。
只允许目录:
python
@click.argument(
"directory",
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
)
只允许文件:
python
@click.argument(
"file",
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
)
核心思想是:参数层能解决的解析和校验,不要拖到业务层解决。
7. Group:从单命令到多命令应用
如果工具只有一个动作,用 @click.command() 就够了。可真实工具通常有多个动作:
bash
tool init
tool build
tool clean
这时用 Group。
python
import click
@click.group()
def cli():
"""项目管理工具。"""
@cli.command()
def init():
"""初始化项目。"""
click.echo("init")
@cli.command()
def build():
"""构建项目。"""
click.echo("build")
这里:
python
@click.group()
创建一个命令组。更准确地说,它会把 cli 这个函数转换成一个 Click Group 对象。Group 本身也是一种命令,只是它不直接完成某个业务动作,而是负责承载和分发下面的子命令。
python
@cli.command()
把函数注册成这个命令组下面的子命令。关键点在 cli.command() 里的 cli:它不是随便起的名字,而是上面那个已经被 @click.group() 转换后的命令组对象。所以:
python
@cli.command()
def init():
...
意思是"把 init 注册到 cli 这个组下面"。同理:
python
@cli.command()
def build():
...
意思是"把 build 也注册到同一个 cli 组下面"。因此最后形成的结构是:
text
cli
├── init
└── build
用户调用时,先进入顶层组,再选择具体子命令:
bash
tool init
tool build
一次命令行调用通常从一个入口命令开始。这个入口命令可以是普通 Command,也可以是 Group:如果工具只有一个动作,用 @click.command();如果工具下面有多个动作,就让入口指向一个 @click.group()。
但这不等于一个 Python 文件或一个项目里只能定义一个 group。Click 官方文档里说,command 可以挂到 group 下,多个 group 也可以继续挂到另一个 group 下,所以 group 可以形成多层结构。最常见的写法是直接在父 group 上继续定义子 group:
python
@click.group()
def cli():
...
@cli.group()
def admin():
...
这样 admin 本身也是一个 group,同时又是 cli 下面的子命令。调用时要先经过顶层入口,再进入子命令组:
bash
tool admin ...
也可以先创建一个独立的命令或命令组,再用 add_command() 挂到某个 group 下面。这种方式常用于把命令拆到多个模块后再统一注册:
python
@click.group()
def cli():
...
@click.group()
def admin():
...
cli.add_command(admin)
所以更准确的说法是:一个命令行入口只有一个起点,但这个起点下面可以挂很多 command,也可以挂很多 group;group 还可以继续嵌套 group。
如果直接运行脚本,可能是:
bash
python app.py init
如果后面配置了正式 entry point,命令名可能就是:
bash
tool init
这里的 tool 不是 Click 自动凭空生成的名字。它可能来自脚本文件名,也可能来自你发布项目时配置的 entry point。Click 负责解析 tool 后面的 init、build、--debug、--port 这些命令和参数;至于最外层命令叫不叫 tool,通常由运行方式或打包配置决定。
默认情况下,Click 会根据被装饰的函数名生成命令名,并把下划线转换成短横线:
python
@cli.command()
def run_server():
...
对应:
bash
tool run-server
如果不想使用默认生成的名字,可以把自定义命令名传给 command():
python
@cli.command("run")
def run_server():
...
对应:
bash
tool run
Group 里还有一个非常重要的规则:参数只属于声明它的那一层。看这个例子:
python
@click.group()
@click.option("--debug", is_flag=True)
def cli(debug):
...
@cli.command()
@click.option("--port", default=8000)
def run(port):
...
正确调用:
bash
tool --debug run --port 9000
错误调用:
bash
tool run --debug --port 9000
原因是:--debug 是定义在顶层 cli group 上的选项,只属于 cli 这一层,所以必须写在子命令 run 前面;--port 是定义在 run 命令上的选项,只属于 run,所以必须写在 run 后面。
这正是 Click 官方文档中 "Group Separation" 讲的规则:某个 command 或 group 上声明的参数只属于它自己,不会自动传递给上层或下层命令。这样做的好处是边界清楚:顶层 group 负责全局参数,子命令负责自己的局部参数。
8. Context:父命令如何把状态传给子命令
上一节说过,group 参数和子命令参数是隔离的。这带来一个问题:如果顶层有 --debug,子命令也想知道它怎么办?
答案是用 Context。
python
@click.group()
@click.option("--debug/--no-debug", default=False)
@click.pass_context
def cli(ctx: click.Context, debug: bool):
ctx.ensure_object(dict)
ctx.obj["debug"] = debug
@cli.command()
@click.pass_context
def sync(ctx: click.Context):
debug = ctx.obj["debug"]
click.echo(f"debug={debug}")
逐行看:
python
@click.pass_context
告诉 Click:调用函数时,把当前上下文对象 ctx 传进来。
python
ctx.ensure_object(dict)
确保 ctx.obj 是一个字典。
python
ctx.obj["debug"] = debug
把顶层 option 的值保存起来,供子命令读取。
调用:
bash
tool --debug sync
sync() 自己没有定义 --debug,但它能通过 ctx.obj 读到顶层状态。
中大型 CLI 更推荐配置对象:
python
class Config:
def __init__(self, debug: bool = False):
self.debug = debug
pass_config = click.make_pass_decorator(Config, ensure=True)
然后:
python
@click.group()
@click.option("--debug/--no-debug", default=False)
@click.pass_context
def cli(ctx, debug):
ctx.obj = Config(debug)
@cli.command()
@pass_config
def sync(config: Config):
click.echo(config.debug)
这种写法比到处写 ctx.obj["debug"] 更清晰。
9. 输入输出:Prompt、Path 和 File
有些值不适合直接写在命令行上,比如用户名、密码、确认操作。Click 支持交互输入。
python
@click.option("--username", prompt=True)
def login(username):
...
如果用户没有传 --username,Click 会询问:
text
Username:
密码输入:
python
@click.password_option()
def login(password):
...
危险操作确认:
python
@click.confirmation_option(prompt="确定要删除所有数据吗?")
def dropdb():
...
文件处理有两个容易混淆的类型:click.Path 和 click.File。
click.Path 只处理路径,不打开文件:
python
@click.argument("source", type=click.Path(exists=True, path_type=Path))
def show(source: Path):
click.echo(source.name)
适合你需要自己控制路径、后缀、目录、输出文件名的场景。
click.File 会打开文件,把文件对象传给函数:
python
@click.argument("input_file", type=click.File("r"))
def show(input_file):
content = input_file.read()
click.echo(content)
写文件:
python
@click.argument("output_file", type=click.File("w"))
def write(output_file):
output_file.write("hello")
click.File 还支持 - 表示 stdin 或 stdout:
bash
tool input.txt -
通常表示把结果写到标准输出。
选择标准:
text
只需要路径:click.Path
想让 Click 打开文件并传入文件对象:click.File
10. 帮助页和错误提示:CLI 的用户界面
用户学习 CLI 的第一入口通常是:
bash
tool --help
Click 会自动生成帮助页,但文案质量取决于你怎么写。
python
@click.command(
short_help="构建项目",
epilog="示例: tool build --release",
)
@click.option("--release", is_flag=True, help="启用发布构建")
@click.option("--jobs", default=4, show_default=True, help="并行任务数")
def build(release: bool, jobs: int):
"""构建当前项目。"""
说明:
python
"""构建当前项目。"""
这是命令说明,会出现在 help 主体中。
python
help="启用发布构建"
这是 option 说明。
python
show_default=True
让默认值显示在帮助页中。
python
short_help="构建项目"
用于命令组的子命令列表。
python
epilog="示例: tool build --release"
适合在帮助页末尾放示例。
注意:click.argument() 没有 help= 参数。位置参数的说明通常写在函数 docstring 里。
参数错误可以用 Click 的异常体系:
python
def validate_ratio(ctx, param, value):
if not 0 <= value <= 1:
raise click.BadParameter("必须在 0 到 1 之间")
return value
@click.option("--ratio", type=float, callback=validate_ratio)
def cli(ratio):
...
业务错误可以用:
python
raise click.ClickException("处理失败")
常见退出码可以这样理解:
text
0:正常执行,或用户查看 --help
1:用户中断、主动 abort、一般运行失败
2:命令行用法错误,例如参数缺失、类型错误
11. 发布和测试
开发阶段可以在文件末尾写:
python
if __name__ == "__main__":
cli()
然后运行:
bash
python app.py --help
但正式 CLI 推荐通过 pyproject.toml 配置 entry point:
toml
[project.scripts]
my-tool = "my_package.cli:cli"
安装后用户直接运行:
bash
my-tool --help
这比 python path/to/app.py 更正式,也更跨平台。安装工具会在 Windows、macOS、Linux 上生成合适的可执行入口,用户不需要知道源码文件在哪里。
测试方面,Click 提供 CliRunner:
python
from click.testing import CliRunner
def test_hello():
runner = CliRunner()
result = runner.invoke(hello, ["--count", "2", "Alice"])
assert result.exit_code == 0
assert "Hello Alice" in result.output
这行:
python
runner.invoke(hello, ["--count", "2", "Alice"])
模拟的是:
bash
hello --count 2 Alice
常用结果字段:
python
result.exit_code
result.output
result.exception
测试子命令:
python
result = runner.invoke(cli, ["--debug", "sync"])
测试交互输入:
python
result = runner.invoke(login, input="alice\nsecret\n")
测试文件系统:
python
with runner.isolated_filesystem():
...
官方提醒:click.testing 会修改解释器状态,只适合测试环境使用,并且不是线程安全的。
12. 完整示例:把概念串起来
先看概念对应表:
text
@click.group() 定义顶层命令组
@cli.command() 定义子命令
@click.argument() 定义位置参数
@click.option() 定义命名选项
click.Path 校验和转换路径
click.Choice 限制枚举值
--foo/--no-foo 成对布尔开关
count=True 支持 -v、-vv、-vvv
@click.pass_context 注入上下文对象
make_pass_decorator 传递配置对象
完整代码:
python
from pathlib import Path
import click
class Config:
def __init__(self, verbose: int = 0):
self.verbose = verbose
pass_config = click.make_pass_decorator(Config, ensure=True)
@click.group()
@click.version_option("1.0.0", prog_name="imgtool")
@click.option("-v", "--verbose", count=True, help="增加日志详细程度")
@click.pass_context
def cli(ctx: click.Context, verbose: int):
"""图片处理命令行工具。"""
ctx.obj = Config(verbose=verbose)
@cli.command()
@click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option("-o", "--output", type=click.Path(path_type=Path), help="输出文件路径")
@click.option("--mode", type=click.Choice(["fast", "safe"]), default="safe", show_default=True)
@click.option("--metadata/--no-metadata", default=True, show_default=True, help="是否保留元数据")
@pass_config
def clean(config: Config, source: Path, output: Path | None, mode: str, metadata: bool):
"""清理 SOURCE 图片并写入输出文件。"""
if config.verbose:
click.echo(f"source={source}")
click.echo(f"mode={mode}")
if output is None:
output = source.with_stem(source.stem + "_clean")
click.echo(f"Clean {source} -> {output}")
click.echo(f"metadata={metadata}")
调用:
bash
imgtool --help
imgtool -vv clean input.png --mode fast --no-metadata
imgtool clean input.png -o output.png
第二条命令中:
bash
imgtool -vv clean input.png --mode fast --no-metadata
Click 会解析出:
python
verbose == 2
source == Path("input.png")
mode == "fast"
metadata == False
verbose 来自顶层 group,最终通过 Config 传给子命令;source、mode、metadata 属于 clean 子命令。
总结
Click 的学习顺序应该是:
text
先理解一个函数如何变成命令
再理解命令行输入如何进入函数参数
再学会区分 argument 和 option
再掌握类型转换、默认值、布尔开关
然后用 Group 组织多个命令
再用 Context 传递全局状态
最后补上输入输出、帮助页、错误处理、发布和测试
这样学下来,Click 就不是一堆装饰器,而是一套清晰的 CLI 应用开发体系。
参考官方文档: