# Python CLI 工具开发实战

**学习目标**：综合运用 Python 知识，使用 argparse 和 click 开发实用的命令行工具，掌握项目打包与分发。

---

## 1. argparse（标准库）

### 1.1 基础用法

```python
# file_manager.py
import argparse
import os
import shutil
from pathlib import Path

def main():
    parser = argparse.ArgumentParser(
        description='文件管理工具',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例:
  %(prog)s list ./photos
  %(prog)s copy file.txt backup/
  %(prog)s info document.pdf
'''
    )
    
    # 子命令
    subparsers = parser.add_subparsers(dest='command', help='可用命令')
    
    # list 命令
    list_parser = subparsers.add_parser('list', help='列出目录内容')
    list_parser.add_argument('path', nargs='?', default='.', help='目录路径')
    list_parser.add_argument('-a', '--all', action='store_true', help='显示隐藏文件')
    list_parser.add_argument('-l', '--long', action='store_true', help='详细格式')
    
    # copy 命令
    copy_parser = subparsers.add_parser('copy', help='复制文件')
    copy_parser.add_argument('source', help='源文件路径')
    copy_parser.add_argument('destination', help='目标路径')
    copy_parser.add_argument('-f', '--force', action='store_true', help='覆盖已存在文件')
    
    # info 命令
    info_parser = subparsers.add_parser('info', help='显示文件信息')
    info_parser.add_argument('file', help='文件路径')
    
    args = parser.parse_args()
    
    if args.command == 'list':
        list_files(args.path, args.all, args.long)
    elif args.command == 'copy':
        copy_file(args.source, args.destination, args.force)
    elif args.command == 'info':
        file_info(args.file)
    else:
        parser.print_help()

def list_files(path, show_all, long_format):
    """列出目录内容"""
    p = Path(path)
    if not p.exists():
        print(f"错误: 路径 '{path}' 不存在")
        return
    
    for item in sorted(p.iterdir()):
        if not show_all and item.name.startswith('.'):
            continue
        
        if long_format:
            size = item.stat().st_size
            is_dir = 'd' if item.is_dir() else '-'
            print(f"{is_dir} {size:>10}  {item.name}")
        else:
            print(item.name)

def copy_file(source, dest, force):
    """复制文件"""
    src = Path(source)
    if not src.exists():
        print(f"错误: 源文件 '{source}' 不存在")
        return
    
    dst = Path(dest)
    if dst.exists() and not force:
        print(f"错误: 目标已存在，使用 -f 覆盖")
        return
    
    shutil.copy2(src, dst)
    print(f"已复制: {source} → {dest}")

def file_info(filepath):
    """显示文件信息"""
    p = Path(filepath)
    if not p.exists():
        print(f"错误: 文件 '{filepath}' 不存在")
        return
    
    stat = p.stat()
    import time
    print(f"文件: {p.name}")
    print(f"路径: {p.resolve()}")
    print(f"大小: {stat.st_size:,} bytes")
    print(f"修改时间: {time.ctime(stat.st_mtime)}")
    print(f"类型: {'目录' if p.is_dir() else '文件'}")

if __name__ == '__main__':
    main()
```

### 1.2 运行效果

```bash
$ python file_manager.py list -l -a ~/Documents
d       4096  .cache
-     153486  notes.md
d       4096  projects

$ python file_manager.py info notes.md
文件: notes.md
路径: /Users/user/Documents/notes.md
大小: 153,486 bytes
修改时间: Mon Jan 15 10:30:00 2024
类型: 文件
```

---

## 2. click（第三方库）

### 2.1 安装与基础

```bash
pip install click
```

```python
# task_manager.py
import click
import json
from pathlib import Path
from datetime import datetime

TASKS_FILE = Path.home() / '.tasks.json'

def load_tasks():
    if TASKS_FILE.exists():
        return json.loads(TASKS_FILE.read_text())
    return []

def save_tasks(tasks):
    TASKS_FILE.write_text(json.dumps(tasks, ensure_ascii=False, indent=2))

@click.group()
@click.version_option('1.0.0')
def cli():
    """任务管理命令行工具"""
    pass

@cli.command()
@click.argument('title')
@click.option('-p', '--priority', type=click.Choice(['high', 'medium', 'low']),
              default='medium', help='优先级')
@click.option('-d', '--due', help='截止日期 (YYYY-MM-DD)')
def add(title, priority, due):
    """添加新任务"""
    tasks = load_tasks()
    task = {
        'id': len(tasks) + 1,
        'title': title,
        'priority': priority,
        'due': due,
        'done': False,
        'created_at': datetime.now().isoformat()
    }
    tasks.append(task)
    save_tasks(tasks)
    click.echo(f"✓ 已添加任务 #{task['id']}: {title}")

@cli.command(name='list')
@click.option('-s', '--status', type=click.Choice(['all', 'pending', 'done']),
              default='all', help='过滤状态')
@click.option('-p', '--priority', type=click.Choice(['high', 'medium', 'low']),
              help='过滤优先级')
def list_tasks(status, priority):
    """列出任务"""
    tasks = load_tasks()
    
    if status == 'pending':
        tasks = [t for t in tasks if not t['done']]
    elif status == 'done':
        tasks = [t for t in tasks if t['done']]
    
    if priority:
        tasks = [t for t in tasks if t['priority'] == priority]
    
    if not tasks:
        click.echo("没有任务")
        return
    
    for t in tasks:
        mark = '✓' if t['done'] else '○'
        color = {'high': 'red', 'medium': 'yellow', 'low': 'green'}[t['priority']]
        click.echo(f"  {mark} #{t['id']:>3}  {t['title']}", )
        if t['due']:
            click.echo(f"        截止: {t['due']}")

@cli.command()
@click.argument('task_id', type=int)
def done(task_id):
    """标记任务完成"""
    tasks = load_tasks()
    for t in tasks:
        if t['id'] == task_id:
            t['done'] = True
            save_tasks(tasks)
            click.echo(f"✓ 已完成: {t['title']}")
            return
    click.echo(f"错误: 任务 #{task_id} 不存在", err=True)

@cli.command()
@click.argument('task_id', type=int)
def delete(task_id):
    """删除任务"""
    tasks = load_tasks()
    original_len = len(tasks)
    tasks = [t for t in tasks if t['id'] != task_id]
    
    if len(tasks) < original_len:
        save_tasks(tasks)
        click.echo(f"✓ 已删除任务 #{task_id}")
    else:
        click.echo(f"错误: 任务 #{task_id} 不存在", err=True)

@cli.command()
def clear():
    """清除已完成任务"""
    tasks = load_tasks()
    pending = [t for t in tasks if not t['done']]
    removed = len(tasks) - len(pending)
    save_tasks(pending)
    click.echo(f"✓ 已清除 {removed} 个已完成任务")

if __name__ == '__main__':
    cli()
```

### 2.2 运行效果

```bash
$ python task_manager.py --help
Usage: task_manager.py [OPTIONS] COMMAND [ARGS]...

  任务管理命令行工具

Options:
  --version  Show the version and exit.
  --help     Show this message and exit.

Commands:
  add      添加新任务
  clear    清除已完成任务
  delete   删除任务
  done     标记任务完成
  list     列出任务

$ python task_manager.py add "完成报告" -p high -d 2024-01-20
✓ 已添加任务 #1: 完成报告

$ python task_manager.py list
  ○ #  1  完成报告
        截止: 2024-01-20

$ python task_manager.py done 1
✓ 已完成: 完成报告
```

---

## 3. Rich（美化输出）

```bash
pip install rich
```

```python
from rich.console import Console
from rich.table import Table
from rich.progress import Progress
from rich.panel import Panel
from rich.markdown import Markdown
import time

console = Console()

# 彩色输出
console.print("[bold red]错误[/bold red]: 文件未找到")
console.print("[green]成功[/green]: 操作完成")
console.print("[yellow]警告[/yellow]: 磁盘空间不足")

# 表格
table = Table(title="任务列表")
table.add_column("ID", style="cyan", justify="right")
table.add_column("任务", style="white")
table.add_column("优先级", style="yellow")
table.add_column("状态", style="green")

table.add_row("1", "完成报告", "高", "待办")
table.add_row("2", "回复邮件", "中", "已完成")
table.add_row("3", "代码审查", "低", "待办")

console.print(table)

# 进度条
with Progress() as progress:
    task = progress.add_task("[cyan]处理中...", total=100)
    while not progress.finished:
        progress.update(task, advance=5)
        time.sleep(0.1)

# 面板
console.print(Panel.fit(
    "[bold]欢迎使用任务管理器[/bold]\n"
    "输入 help 查看可用命令",
    title="Task Manager",
    border_style="blue"
))

# Markdown 渲染
md = Markdown("""
# 说明
- 支持 **Markdown** 语法
- 自动渲染为终端富文本
""")
console.print(md)
```

---

## 4. 项目打包

### 4.1 pyproject.toml 配置

```toml
[project]
name = "task-manager"
version = "1.0.0"
description = "一个简单的任务管理命令行工具"
authors = [{name = "Your Name", email = "you@example.com"}]
dependencies = [
    "click>=8.0",
    "rich>=13.0",
]

[project.scripts]
task = "task_manager.cli:cli"

[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.backends._legacy:_Backend"

[tool.setuptools.packages.find]
where = ["src"]
```

### 4.2 安装与使用

```bash
# 开发模式安装
pip install -e .

# 安装后直接使用命令
task add "买牛奶" -p low
task list
task done 1

# 构建分发包
pip install build
python -m build

# 发布到 PyPI
pip install twine
twine upload dist/*
```

---

## 5. 小结

| 工具 | 特点 | 适用场景 |
|------|------|----------|
| argparse | 标准库，无需安装 | 简单工具 |
| click | 装饰器风格，功能强大 | 中大型 CLI |
| Rich | 终端美化 | 增强输出 |
| Typer | 基于 click + 类型提示 | 现代项目 |

---

## 6. 练习题

1. 使用 click 实现一个密码生成器 CLI，支持指定长度、字符集。
2. 使用 Rich 美化你的 CLI 工具输出，添加表格和进度条。
3. 为 CLI 工具添加配置文件支持（读取 ~/.mytoolrc）。
4. 将你的工具打包并发布到 TestPyPI。

---

> **下节预告**：我们将使用 requests 和 BeautifulSoup 实现一个完整的 Web 爬虫。

