Python 编程教程 / 20 - CLI 开发
第 20 章:CLI 开发
使用 Python 构建专业的命令行工具,从 argparse 到 Typer。
20.1 argparse(标准库)
20.1.1 基本用法
import argparse
def main():
parser = argparse.ArgumentParser(description="文件处理工具")
parser.add_argument("input", help="输入文件路径")
parser.add_argument("-o", "--output", help="输出文件路径")
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
parser.add_argument("--count", type=int, default=1, help="处理次数")
args = parser.parse_args()
if args.verbose:
print(f"处理文件: {args.input}")
print(f"次数: {args.count}")
if __name__ == "__main__":
main()
$ python cli.py data.txt -o result.txt -v --count 3
$ python cli.py --help
20.1.2 子命令
import argparse
def main():
parser = argparse.ArgumentParser(prog="tool")
subparsers = parser.add_subparsers(dest="command", help="子命令")
# create 子命令
create_parser = subparsers.add_parser("create", help="创建资源")
create_parser.add_argument("name", help="资源名称")
create_parser.add_argument("--type", default="default", help="资源类型")
# list 子命令
list_parser = subparsers.add_parser("list", help="列出资源")
list_parser.add_argument("--all", action="store_true", help="显示全部")
args = parser.parse_args()
if args.command == "create":
print(f"创建 {args.type}: {args.name}")
elif args.command == "list":
print(f"列出资源 (all={args.all})")
if __name__ == "__main__":
main()
20.2 Click
20.2.1 基本用法
import click
@click.group()
@click.version_option(version="1.0.0")
def cli():
"""文件处理工具。"""
pass
@cli.command()
@click.argument("input", type=click.Path(exists=True))
@click.option("-o", "--output", type=click.Path(), help="输出文件")
@click.option("-v", "--verbose", is_flag=True, help="详细输出")
def process(input, output, verbose):
"""处理文件。"""
if verbose:
click.echo(f"处理: {input}")
with open(input) as f:
data = f.read()
click.echo(f"读取 {len(data)} 字符")
@cli.command()
@click.argument("names", nargs=-1)
def greet(names):
"""问候用户。"""
for name in names:
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
cli()
20.2.2 Click 高级特性
import click
@click.command()
@click.option("--name", prompt="Your name", help="你的名字")
@click.option("--count", default=1, type=click.IntRange(1, 10), help="次数")
@click.option("--color", type=click.Choice(["red", "green", "blue"]), default="red")
@click.password_option()
def hello(name, count, color, password):
"""带交互的 CLI。"""
for _ in range(count):
click.secho(f"Hello, {name}!", fg=color)
# 进度条
@click.command()
def process():
"""带进度条的处理。"""
items = range(100)
with click.progressbar(items, label="处理中") as bar:
for item in bar:
pass # 处理每个项目
if __name__ == "__main__":
hello()
20.3 Typer(推荐)
20.3.1 基本用法
import typer
from typing import Optional
app = typer.Typer(help="文件处理工具")
@app.command()
def process(
input: str = typer.Argument(..., help="输入文件"),
output: Optional[str] = typer.Option(None, "-o", "--output", help="输出文件"),
verbose: bool = typer.Option(False, "-v", "--verbose", help="详细输出"),
count: int = typer.Option(1, "--count", min=1, max=10, help="处理次数"),
):
"""处理文件。"""
if verbose:
typer.echo(f"处理: {input}")
for i in range(count):
typer.echo(f"第 {i + 1} 次处理完成")
if __name__ == "__main__":
app()
$ python cli.py process data.txt -v --count 3
$ python cli.py --help
$ python cli.py process --help
20.3.2 子命令
import typer
app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app, name="users")
@users_app.command("create")
def users_create(name: str, email: str = typer.Option(...)):
"""创建用户。"""
typer.echo(f"创建用户: {name} ({email})")
@users_app.command("list")
def users_list(all: bool = typer.Option(False, "--all")):
"""列出用户。"""
typer.echo(f"列出用户 (all={all})")
@app.command()
def version():
"""显示版本。"""
typer.echo("v1.0.0")
if __name__ == "__main__":
app()
20.3.3 Typer 美化输出
import typer
app = typer.Typer()
@app.command()
def demo():
# 颜色输出
typer.secho("成功!", fg=typer.colors.GREEN, bold=True)
typer.secho("警告!", fg=typer.colors.YELLOW)
typer.secho("错误!", fg=typer.colors.RED)
# 确认
if typer.confirm("是否继续?"):
typer.echo("继续执行")
else:
raise typer.Abort()
# 选择
options = ["Python", "Go", "Rust"]
choice = typer.prompt("选择语言", type=typer.Choice(options))
typer.echo(f"选择了: {choice}")
# 启动/失败指示器
with typer.progressbar(range(100)) as progress:
for value in progress:
pass
typer.echo("完成!")
if __name__ == "__main__":
app()
20.4 Rich(终端美化)
20.4.1 基本输出
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import track
import time
console = Console()
# 彩色输出
console.print("[bold red]错误[/bold red]: 文件不存在")
console.print("[green]成功[/green]: 操作完成")
# 表格
table = Table(title="用户列表")
table.add_column("ID", style="cyan")
table.add_column("姓名", style="magenta")
table.add_column("城市", style="green")
table.add_row("1", "Alice", "北京")
table.add_row("2", "Bob", "上海")
console.print(table)
# 面板
console.print(Panel("Hello, World!", title="欢迎", border_style="blue"))
# 进度条
for step in track(range(100), description="处理中..."):
time.sleep(0.02)
20.4.2 Rich + Typer
import typer
from rich.console import Console
from rich.table import Table
app = typer.Typer()
console = Console()
@app.command()
def list_users():
"""列出用户。"""
table = Table(title="用户列表")
table.add_column("ID", style="cyan")
table.add_column("姓名", style="bold")
table.add_column("状态", style="green")
users = [("1", "Alice", "活跃"), ("2", "Bob", "离线")]
for user in users:
table.add_row(*user)
console.print(table)
if __name__ == "__main__":
app()
20.5 交互式输入
import typer
from InquirerPy import inquirer
app = typer.Typer()
@app.command()
def setup():
"""交互式配置。"""
name = inquirer.text(message="项目名称:").execute()
framework = inquirer.select(
message="选择框架:",
choices=["FastAPI", "Flask", "Django"],
).execute()
features = inquirer.checkbox(
message="选择特性:",
choices=["数据库", "认证", "缓存", "日志"],
).execute()
typer.echo(f"\n配置: {name}, {framework}, {features}")
if __name__ == "__main__":
app()
20.6 框架选型
| 框架 | 学习曲线 | 类型注解 | 自动帮助 | 推荐场景 |
|---|---|---|---|---|
| argparse | 低 | ❌ | 手动 | 简单脚本 |
| Click | 中 | ❌ | ✅ | 复杂 CLI |
| Typer | 低 | ✅ | ✅ | 推荐首选 |
| Fire | 低 | ❌ | 自动 | 快速原型 |
20.7 注意事项
🔴 注意:
- CLI 工具要有清晰的帮助文档
- 参数要有合理的默认值
- 失败时返回非零退出码
- 输出要适合管道处理(stdout 只输出数据)
💡 提示:
- Typer 基于 Click 构建,支持类型注解
- Rich 可以美化任何终端输出
- 使用
typer.Exit(1)返回错误码 - 使用
click.get_text_stream()处理管道输入
📌 业务场景:
import typer
from rich.console import Console
from pathlib import Path
app = typer.Typer(help="日志分析工具")
console = Console()
@app.command()
def analyze(
logfile: Path = typer.Argument(..., help="日志文件路径", exists=True),
level: str = typer.Option("ERROR", "--level", "-l", help="最低日志级别"),
output: Path = typer.Option(None, "--output", "-o", help="输出文件"),
top: int = typer.Option(10, "--top", "-n", help="显示前 N 条"),
):
"""分析日志文件。"""
levels = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3, "CRITICAL": 4}
min_level = levels.get(level, 3)
errors = []
with open(logfile) as f:
for line in f:
for lvl, priority in levels.items():
if lvl in line and priority >= min_level:
errors.append(line.strip())
break
console.print(f"[bold]找到 {len(errors)} 条 {level}+ 日志[/bold]")
for err in errors[:top]:
if "ERROR" in err:
console.print(f" [red]{err}[/red]")
elif "WARNING" in err:
console.print(f" [yellow]{err}[/yellow]")
else:
console.print(f" {err}")
if __name__ == "__main__":
app()