Python Click 教程:从函数到专业命令行工具

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 会把它转换成函数参数 countdefault=1 表示用户不传时默认是 1help="重复次数" 会出现在 --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", ...) 不能随便省。因为函数签名需要 countname,命令行接口也希望支持:

bash 复制代码
python hello.py --count 3 Alice

如果只保留 @click.command(),却仍然写成:

python 复制代码
@click.command()
def hello(count: int, name: str) -> None:
    ...

Click 会创建一个"不接收任何参数"的命令。它不知道应该从命令行里读取 countname,也就无法正确调用这个函数。

还要注意,"可选"这个词在这里有两层意思。@click.argument(...)@click.option(...) 作为装饰器是按需使用的;但它们声明出来的命令行参数是否必填,是另一回事。默认情况下,argument 通常是必填的位置参数,而 option 通常是可以省略的命名选项,除非你给 option 设置 required=True

最后看函数签名:

python 复制代码
def hello(count: int, name: str) -> None:

这里的 countname 不是你从 sys.argv 里手动取出来的,而是 Click 根据前面的装饰器解析后传入的。可以把它理解成:命令行里的 --count 3 Alice,最后会被 Click 整理成一次普通的 Python 函数调用:

python 复制代码
hello(count=3, name="Alice")

这里有两个容易混淆的点。

第一,函数参数名要和 Click 生成的参数名对得上。@click.option("--count", ...) 默认会生成名为 count 的函数参数,@click.argument("name") 会生成名为 name 的函数参数,所以函数签名里写的是 countname。如果你把函数写成 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 这类开关。用户只需要写出选项名,不需要再写 truefalse

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 后面的 initbuild--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.Pathclick.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 传给子命令;sourcemodemetadata 属于 clean 子命令。

总结

Click 的学习顺序应该是:

text 复制代码
先理解一个函数如何变成命令
再理解命令行输入如何进入函数参数
再学会区分 argument 和 option
再掌握类型转换、默认值、布尔开关
然后用 Group 组织多个命令
再用 Context 传递全局状态
最后补上输入输出、帮助页、错误处理、发布和测试

这样学下来,Click 就不是一堆装饰器,而是一套清晰的 CLI 应用开发体系。

参考官方文档:

相关推荐
Karle_1 小时前
为AI编辑器准备c++编译环境,onnxruntime、cmake、cl,网上坑太多备份记录后续方便使用。
开发语言·c++·编辑器
Dxy12393102161 小时前
JavaScript 字符串转数值(小数)
开发语言·javascript·ecmascript
u0119608231 小时前
ray 依赖分发
python
yu85939581 小时前
matlab实现ARMA(自回归移动平均)模型
开发语言·matlab·回归
lbb 小魔仙1 小时前
Ollama 本地部署大模型 + Python API 集成开发完整教程(2026最新版,含 GPU 加速配置)
开发语言·python
DanCheng-studio2 小时前
毕设分享 深度学习遮挡下的人脸识别(源码+论文)
python·毕业设计·毕设
小妖同学学AI2 小时前
告别手动盯盘!开源框架Freqtrade,教你用Python打造“永不下班”的AI交易员
人工智能·python·开源
民乐团扒谱机2 小时前
【微实验】平滑轨迹的数学基石:二次贝塞尔曲线原理、插值逻辑、形态控制与MATLAB全解析
开发语言·matlab
CSCN新手听安2 小时前
【Qt】Qt窗口(七)QColorDialog颜色对话框,QFileDialog文件对话框的使用
开发语言·c++·qt