命令行工具是开发者日常使用频率最高的工具之一,Python 生态中有丰富的库来构建 CLI。很多人习惯手写 argparse,但随着命令变复杂,维护成本急剧上升。现代 Python CLI 开发应直接选择 Click 或 Typer,它们各有优势。
- import click
- @click.command()
- @click.option("--count", "-c", default=1, help="重复次数")
- @click.option("--uppercase", "-u", is_flag=True, help="大写输出")
- @click.argument("name")
- def hello(count, uppercase, name):
- """向 NAME 打招呼"""
- greeting = f"Hello, {name}!"
- if uppercase:
- greeting = greeting.upper()
- for _ in range(count):
- click.echo(greeting)
复制代码
项目结构从一开始就要清晰。推荐标准布局:- my-cli/
- ├── pyproject.toml
- ├── README.md
- ├── src/
- │ └── mycli/
- │ ├── __init__.py
- │ ├── cli.py
- │ ├── commands/
- │ │ ├── __init__.py
- │ │ ├── deploy.py
- │ │ └── config.py
- │ ├── core/
- │ └── utils.py
- └── tests/
- ├── test_cli.py
- └── test_core.py
复制代码
关键原则:commands/ 只负责解析参数、调用 core/,不写业务逻辑;使用 src/ 布局避免导入歧义;pyproject.toml 统一管理依赖和入口点。
命令设计要遵循一致性。Simon Willison 在多个 CLI 项目后总结出:Arguments 用于必填位置参数,Options 用于可选配置,Flags 是不带值的布尔开关,子命令用于功能分组。每个命令必须有 --help 文档,设计新选项前参考 git、docker 等工具的惯用法。
配置管理采用分层优先级:CLI 参数 > 环境变量 > .env 文件 > 默认值。Pydantic Settings 能自动从环境变量和 .env 文件读取,并做类型校验:- from pydantic_settings import BaseSettings
- class AppConfig(BaseSettings):
- api_key: str
- timeout: int = 30
- debug: bool = False
- model_config = {"env_file": ".env", "env_prefix": "MYAPP_"}
复制代码
输出体验用 Rich 库实现。使用过程中注意:用 click.echo() 而非 print();正常输出走 stdout,错误和日志走 stderr;长任务用 rich.progress 给出反馈;支持 --quiet / --verbose 级别;检测 TTY 环境,在 CI 中自动关闭颜色。
错误处理要优雅。用户错误如参数错误、文件不存在,给出清晰提示并 sys.exit(1),不打印 traceback。程序错误记录到日志,终端只显示简洁摘要。退出码遵循 POSIX:0 成功,1 通用错误,2 参数错误。Click 的 type=click.Path(exists=True) 能在参数解析阶段拦截错误。- import click, sys
- @click.command()
- @click.argument("filepath", type=click.Path(exists=True))
- def process(filepath):
- try:
- pass # 业务逻辑
- except PermissionError:
- click.echo(f"错误:无权访问 {filepath}", err=True)
- sys.exit(1)
复制代码
测试利用 Click 提供的 CliRunner 在不启动真实进程的情况下模拟命令调用:- from click.testing import CliRunner
- from mycli.cli import main
- def test_hello_command():
- runner = CliRunner()
- result = runner.invoke(main, ["--count", "2", "World"])
- assert result.exit_code == 0
- assert "Hello, World!" in result.output
- assert result.output.count("Hello") == 2
复制代码
测试策略:单元测试覆盖核心业务逻辑,集成测试用 CliRunner 覆盖主要命令路径,用 pytest + pytest-cov 保持覆盖率,测试空输入、文件不存在、权限不足等边界情况。
打包发布全部收敛到 pyproject.toml:- [project]
- name = "my-awesome-cli"
- version = "1.0.0"
- requires-python = ">=3.9"
- dependencies = ["click>=8.0", "rich>=13.0", "pydantic-settings>=2.0"]
- [project.scripts]
- mycli = "mycli.cli:main"
- [build-system]
- requires = ["hatchling"]
- build-backend = "hatchling.build"
复制代码
[project.scripts] 是关键,用户 pip install 后可直接在终端敲 mycli。发布到 PyPI 的流程:- pip install build twine
- python -m build
- twine upload dist/*
复制代码
更多让工具更好用的细节:Shell 自动补全(Typer 内置,Click 额外配置);--version 标志;--dry-run 模式;支持从 stdin 读取管道输入;用 Cookiecutter 模板起步(如 simonw/click-app)。总结:框架选 Click(精细控制)或 Typer(快速开发),配置用 Pydantic Settings,终端输出用 Rich,打包用 pyproject.toml + hatchling,测试用 pytest + Click CliRunner,发布用 twine 或 GitHub Actions CI。 |