CLI 命令行与 Python 框架实战

CLI 命令行与 Python 框架实战

第 31 期 · 建议学习 4-6 小时

Python 3.10+ · 可复制运行 · 工程化实践

把 Python 脚本做成可靠、可发现、可测试的命令行工具。本章覆盖命令设计、参数解析、帮助文档,以及 argparse、Click、Typer 三套实现方式。

目录

1. CLI 是什么,何时该用

CLI(Command Line Interface)以命令、参数和标准输入输出驱动程序。它适合批处理、自动化、开发者工具、运维任务和 CI/CD。

价值 说明
可重复 同一命令与同一参数,可在本地、服务器和 CI 中重复执行。
可组合 输出可写入文件、管道,或交给另一个程序。
可自动化 定时任务、Shell 脚本与部署流水线都能稳定调用。

判断方法:如果用户要反复执行同一个动作,或该动作要被脚本、流水线、其他开发者调用,就应该定义一个清晰的 CLI 契约。一次性探索脚本不必过早包装。

2. 完整 CLI 的使用样式

bash 复制代码
acme report sales.csv --format json --output result.json --verbose
# 程序名  子命令  必填输入       可选配置              行为开关
部分 示例 作用
程序名 acme 工具的安装入口。
子命令 report 要执行的动作。
位置参数 sales.csv 核心、必填的输入。
选项 --format json 有默认值或改变行为的配置。
标志 --verbose 启用或关闭某项行为。

参数设计规则

  • 动作放在子命令:create、list、export。
  • 位置参数只保留真正必需的输入:tool convert input.csv。
  • 有默认值或改变行为的值用选项:--output、--limit。
  • 布尔开关使用 --verbose、--dry-run,不要写 --verbose true。
  • 命令与选项使用小写连字符:create-user、--dry-run。
  • 危险操作先展示影响范围,要求确认或显式使用 --yes。
  • 面向脚本时提供 --format json,标准输出只输出 JSON。
  • 稳定支持 --help、--version 和退出码。

3. Python CLI 框架选型

先按复杂度选择。标准库适合小工具,Click 适合成熟多命令工具,Typer 适合新项目与类型注解驱动的接口。

方案 适用场景 优点 取舍 建议
argparse 小工具、无依赖场景 Python 自带,理解 CLI 基础 复杂子命令稍冗长 基础必学
Click 成熟 CLI、多子命令 装饰器清晰,生态成熟 类型意图不如 Typer 直接 生产常用
Typer 新项目、类型注解 自动参数校验与帮助,基于 Click 仍需理解命令接口设计 优先推荐
Rich 表格、进度条、日志 人类输出更易读 不是参数解析器 配合使用
questionary 交互式初始化向导 选择与确认体验好 CI 需提供无交互参数 按需加入

4. 标准库方案:argparse

argparse 解析参数、生成 --help、校验枚举值,且不需要安装依赖。这是学习命令树的最佳入口。

文件:todo_argparse.py

python 复制代码
from __future__ import annotations

import argparse
from pathlib import Path


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="todo",
        description="一个最小待办事项 CLI",
    )
    commands = parser.add_subparsers(dest="command", required=True)

    add = commands.add_parser("add", help="新增一条待办")
    add.add_argument("title", help="待办标题")
    add.add_argument(
        "--priority",
        choices=["low", "normal", "high"],
        default="normal",
    )
    add.add_argument("--file", type=Path, default=Path("todo.txt"))

    listing = commands.add_parser("list", help="列出待办")
    listing.add_argument("--file", type=Path, default=Path("todo.txt"))
    return parser


def main() -> int:
    args = build_parser().parse_args()

    if args.command == "add":
        with args.file.open("a", encoding="utf-8") as f:
            f.write(f"[{args.priority}] {args.title}\n")
        print(f"已写入:{args.title}")
    elif args.command == "list":
        print(args.file.read_text(encoding="utf-8") if args.file.exists() else "暂无待办")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

使用方法:

bash 复制代码
python todo_argparse.py --help
python todo_argparse.py add "学习 CLI" --priority high
python todo_argparse.py list
python todo_argparse.py add "准备分享" --file work.txt

5. 工程常用:Click

Click 用装饰器声明命令。命令组、子命令、选项和上下文边界清晰,适合有多个功能的工具。

安装:

bash 复制代码
python -m pip install click

文件:user_cli.py

python 复制代码
import json
from pathlib import Path

import click

DATA_FILE = Path("users.json")


def load_users() -> list[dict]:
    return json.loads(DATA_FILE.read_text()) if DATA_FILE.exists() else []


@click.group()
@click.option("--verbose", is_flag=True, help="输出额外执行信息")
@click.pass_context
def cli(ctx: click.Context, verbose: bool) -> None:
    """用户管理工具。"""
    ctx.ensure_object(dict)
    ctx.obj["verbose"] = verbose


@cli.command()
@click.argument("name")
@click.option(
    "--role",
    type=click.Choice(["viewer", "editor", "admin"]),
    default="viewer",
)
@click.pass_context
def add(ctx: click.Context, name: str, role: str) -> None:
    """创建用户。"""
    users = load_users()
    users.append({"name": name, "role": role})
    DATA_FILE.write_text(json.dumps(users, ensure_ascii=False, indent=2))

    if ctx.obj["verbose"]:
        click.echo(f"写入文件:{DATA_FILE.resolve()}")
    click.echo(f"已创建用户:{name} ({role})")


@cli.command("list")
@click.option(
    "--format",
    "output_format",
    type=click.Choice(["table", "json"]),
    default="table",
)
def list_users(output_format: str) -> None:
    """列出用户。"""
    users = load_users()

    if output_format == "json":
        click.echo(json.dumps(users, ensure_ascii=False))
        return

    for user in users:
        click.echo(f"{user['name']:<16} {user['role']}")


if __name__ == "__main__":
    cli()

完整使用:

bash 复制代码
python user_cli.py --help
python user_cli.py add Alice --role admin
python user_cli.py --verbose add Bob
python user_cli.py list --format json

方法:ctx.obj 可向所有子命令传递公共配置,如 --verbose、配置文件路径或 API 客户端。命令函数只做参数转换和调用,业务逻辑应放在普通 Python 函数中。

6. 新项目推荐:Typer + Rich

Typer 从类型注解生成参数解析与帮助文档。与 Path、date、Enum 和 Pydantic 等类型配合很自然,Rich 用于人类可读的终端反馈。

安装:

bash 复制代码
python -m pip install typer rich

文件:report_cli.py

python 复制代码
from datetime import date
from enum import Enum
from pathlib import Path
from typing import Annotated

import typer
from rich.console import Console

app = typer.Typer(help="日报生成工具")
console = Console()


class OutputFormat(str, Enum):
    text = "text"
    json = "json"


@app.command()
def generate(
    source: Annotated[
        Path,
        typer.Argument(exists=True, readable=True, help="原始 CSV 文件"),
    ],
    day: Annotated[
        date,
        typer.Option("--day", help="YYYY-MM-DD"),
    ] = date.today(),
    output: Annotated[
        Path,
        typer.Option("--output", "-o"),
    ] = Path("report.txt"),
    output_format: Annotated[
        OutputFormat,
        typer.Option("--format"),
    ] = OutputFormat.text,
    dry_run: Annotated[
        bool,
        typer.Option("--dry-run"),
    ] = False,
) -> None:
    """根据输入文件生成日报。"""
    content = (
        f"日报日期:{day}\n"
        f"数据来源:{source.name}\n"
        f"格式:{output_format.value}\n"
    )

    if dry_run:
        console.print(f"[yellow]演练模式,不写入:{output}[/yellow]")
        console.print(content)
        return

    output.write_text(content, encoding="utf-8")
    console.print(f"[green]已生成:{output.resolve()}[/green]")


@app.command()
def status() -> None:
    """显示工具状态。"""
    console.print("[green]report-cli 可用:generate, status[/green]")


if __name__ == "__main__":
    app()

完整使用:

bash 复制代码
python report_cli.py --help
python report_cli.py generate input.csv --day 2026-07-26 -o daily.txt
python report_cli.py generate input.csv --dry-run
python report_cli.py status

7. 从脚本到可安装 CLI

工具交给其他人使用时,用 pyproject.toml 注册入口。安装后用户运行 report-cli,不需要知道脚本路径或 Python 模块结构。

text 复制代码
report-cli/
├── pyproject.toml
├── src/
│   └── report_cli/
│       ├── __init__.py
│       ├── cli.py        # Typer / Click 命令层
│       └── service.py    # 可复用业务逻辑
└── tests/
    └── test_cli.py

文件:pyproject.toml

toml 复制代码
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "report-cli"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["typer>=0.12", "rich>=13"]

[project.scripts]
report-cli = "report_cli.cli:app"

[tool.pytest.ini_options]
testpaths = ["tests"]

安装、运行与构建:

bash 复制代码
python -m venv .venv
source .venv/bin/activate              # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -e .

report-cli --help
report-cli generate input.csv --dry-run

python -m pip install build
python -m build

Rich、questionary、FastAPI 的组合

  • Rich:用于表格、进度条、彩色日志。面向人类的输出可以丰富,面向机器的输出必须稳定。
  • questionary:适合 init 向导和危险操作确认,必须给 CI 提供参数或 --non-interactive 回退。
  • FastAPI:CLI 与 API 不必二选一。把核心能力放在 service.py,Typer 命令与 FastAPI 路由各自完成输入转换,再调用同一业务函数。

8. 质量、错误与发布

退出码与安全

  • 0:成功。
  • 1:业务失败,例如找不到指定资源。
  • 2:命令使用错误,通常由参数解析器处理。
  • 危险操作先显示受影响对象,提供 --dry-run 与 --yes。
  • 不要输出密码、令牌和完整密钥。仅在 --debug 下输出必要的异常细节。
  • 选项优先于环境变量,环境变量优先于配置文件。

Typer CLI 测试示例

文件:tests/test_cli.py

python 复制代码
from typer.testing import CliRunner

from report_cli.cli import app

runner = CliRunner()


def test_status() -> None:
    result = runner.invoke(app, ["status"])

    assert result.exit_code == 0
    assert "report-cli" in result.stdout


def test_missing_file() -> None:
    result = runner.invoke(app, ["generate", "missing.csv"])

    assert result.exit_code != 0

发布前检查清单

  • tool --help 能解释每个命令、参数、默认值和示例。
  • 测试成功路径、非法参数、文件不存在与 --dry-run。
  • 为脚本使用者提供 --format json,不要混入进度文字。
  • 在新虚拟环境中完成安装、--version、基本命令与打包安装验证。
  • 不在日志、错误信息、示例或截图中泄露令牌、密码和密钥。

本期作业

选择一个已有 Python 脚本,将它改造成如下形式的 CLI:

bash 复制代码
your-tool action input --dry-run

交付:

  1. --help 的文本或截图。
  2. 三条真实调用命令。
  3. 至少一个 CLI 自动化测试。
  4. 一条失败场景及其退出码说明。

常见问题

Click、Typer、argparse 能混用吗?

同一条命令树建议只使用一种参数解析框架,避免帮助文本和错误处理不一致。业务逻辑、Rich 输出、Pydantic 模型和 pytest 测试可以共享。已有 Click 项目无需为了类型注解强行迁移。

什么时候要用子命令?

当工具有两个以上独立动作,例如 user add、user list、user delete 时使用子命令。只有一个动作且参数很少时,单层命令更好用。

为什么要区分人类输出和 JSON 输出?

人类输出可以有颜色、表格、进度条;脚本需要稳定字段和可预测结构。提供 --format json 后,应保持标准输出只包含 JSON,错误信息输出到标准错误。

相关推荐
fthux1 小时前
GitHub Actions自动化运维实战:构建高效可靠的CI/CD流水线
运维·自动化·github
Maiko Star1 小时前
FastAPI 进阶三部曲:中间件、依赖注入与 ORM 实战
python·中间件·fastapi
MrDJun1 小时前
长期稳定跑网页监控:TLS 指纹、代理选路与请求节流的工程实践
运维·爬虫·python·网络协议·网站监控
m沐沐1 小时前
【计算机视觉】OpenCV 物体跟踪——原理、算法与CSRT跟踪器实战
人工智能·python·深度学习·opencv·算法·计算机视觉·人脸识别
大数据魔法师2 小时前
AI Agent - OpenAI从零开始完整学习教程(零基础入门+实战落地)
python
卷无止境2 小时前
Python的魔术方法:那些藏在双下划线背后的魔法
后端·python
米码收割机2 小时前
【Python】Django恒达科技门户网站(源码+文档)【独一无二】
数据库·python·科技
卷无止境2 小时前
别再被"乱码"吓到了:Python文件操作的门道
后端·python
zzzzzz3102 小时前
我用 AI Agent 重构了日常开发工作流,效果出乎意料
人工智能·git·github