🐍 第13课:命令行参数

—— argparse:专业 CLI 工具

🏆 CLI工具完整参数
✅ Python验证通过

📌 本课目标

1️⃣ argparse 基础

import argparse

parser = argparse.ArgumentParser(
    description="数据处理工具",
    epilog="示例: python tool.py input.csv --limit 100",
)

# 位置参数(必填)
parser.add_argument("input", help="输入文件路径")

# 可选参数
parser.add_argument("-o", "--output", default="output.csv",
                    help="输出文件路径")
parser.add_argument("-n", "--limit", type=int, default=100,
                    help="处理行数限制")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="详细输出模式")

args = parser.parse_args()
print(args.input)    # 输入文件
print(args.output)   # output.csv
print(args.limit)    # 100
print(args.verbose)  # False

2️⃣ 参数类型与验证

import argparse
from pathlib import Path

def positive_int(value):
    """自定义类型:正整数"""
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError(f"{value} 不是正整数")
    return ivalue

def existing_file(value):
    """自定义类型:已存在的文件"""
    path = Path(value)
    if not path.is_file():
        raise argparse.ArgumentTypeError(f"文件不存在: {value}")
    return path

parser = argparse.ArgumentParser()

# 类型转换
parser.add_argument("--count", type=int)
parser.add_argument("--rate", type=float)
parser.add_argument("--limit", type=positive_int)

# 选择列表
parser.add_argument("--format", choices=["csv", "json", "yaml"],
                    default="csv")

# 文件参数
parser.add_argument("input", type=existing_file)
parser.add_argument("-o", type=Path, default=Path("output.csv"))

# 计数参数
parser.add_argument("-v", "--verbose", action="count", default=0)
# -v → 1, -vv → 2, -vvv → 3

# 多值参数
parser.add_argument("--tags", nargs="+", help="一个或多个标签")
parser.add_argument("--ids", nargs=3, type=int, help="恰好3个ID")

# 互斥参数
group = parser.add_mutually_exclusive_group()
group.add_argument("--json", action="store_true", help="JSON输出")
group.add_argument("--yaml", action="store_true", help="YAML输出")

3️⃣ 子命令(subparsers)

import argparse

parser = argparse.ArgumentParser(prog="mycli", description="数据管理工具")
subparsers = parser.add_subparsers(dest="command", help="可用命令")

# init 子命令
init_parser = subparsers.add_parser("init", help="初始化项目")
init_parser.add_argument("--name", required=True, help="项目名")
init_parser.add_argument("--template", choices=["basic", "advanced"],
                         default="basic")

# sync 子命令
sync_parser = subparsers.add_parser("sync", help="同步数据")
sync_parser.add_argument("source", help="数据源")
sync_parser.add_argument("--dry-run", action="store_true")
sync_parser.add_argument("--batch-size", type=int, default=1000)

# export 子命令
export_parser = subparsers.add_parser("export", help="导出数据")
export_parser.add_argument("--format", choices=["csv", "json"],
                           default="csv")
export_parser.add_argument("--output", "-o", default="export")

# 解析
args = parser.parse_args()
# mycli init --name myproject
# mycli sync database --dry-run --batch-size 500
# mycli export --format json -o data

4️⃣ 实战:文件处理 CLI

#!/usr/bin/env python3
"""fileproc - 文件处理 CLI 工具"""
import argparse
import json
import csv
import sys
from pathlib import Path

def cmd_info(args):
    """显示文件信息"""
    path = Path(args.file)
    if not path.exists():
        print(f"错误: {args.file} 不存在", file=sys.stderr)
        sys.exit(1)
    info = {
        "路径": str(path.resolve()),
        "类型": "目录" if path.is_dir() else "文件",
        "大小": path.stat().st_size if path.is_file() else "N/A",
    }
    if args.json:
        print(json.dumps(info, ensure_ascii=False, indent=2))
    else:
        for k, v in info.items():
            print(f"{k}: {v}")

def cmd_convert(args):
    """CSV → JSON 转换"""
    with open(args.input, encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        rows = list(reader)

    output = Path(args.output)
    with open(output, "w", encoding="utf-8") as f:
        json.dump(rows, f, ensure_ascii=False, indent=2)
    print(f"转换完成: {len(rows)} 行 → {output}")

def main():
    parser = argparse.ArgumentParser(
        prog="fileproc",
        description="文件处理 CLI 工具",
    )
    sub = parser.add_subparsers(dest="command")

    # info 子命令
    p_info = sub.add_parser("info", help="显示文件信息")
    p_info.add_argument("file", help="文件路径")
    p_info.add_argument("--json", action="store_true")

    # convert 子命令
    p_conv = sub.add_parser("convert", help="格式转换")
    p_conv.add_argument("input", help="输入文件")
    p_conv.add_argument("-o", "--output", default="output.json")

    args = parser.parse_args()
    if args.command == "info":
        cmd_info(args)
    elif args.command == "convert":
        cmd_convert(args)
    else:
        parser.print_help()

if __name__ == "__main__":
    main()

5️⃣ 验证脚本

#!/usr/bin/env python3
"""第13课 命令行参数验证"""
import argparse

def test_basic_parse():
    """基本解析测试"""
    parser = argparse.ArgumentParser()
    parser.add_argument("input")
    parser.add_argument("-o", "--output", default="out.txt")
    parser.add_argument("-v", "--verbose", action="store_true")
    args = parser.parse_args(["data.csv", "-o", "result.csv"])
    assert args.input == "data.csv"
    assert args.output == "result.csv"
    assert args.verbose == False
    print("✅ 基本解析测试通过")

def test_type_validation():
    """类型验证测试"""
    parser = argparse.ArgumentParser()
    parser.add_argument("--count", type=int, default=0)
    parser.add_argument("--rate", type=float, default=0.0)
    parser.add_argument("--format", choices=["csv", "json"], default="csv")
    args = parser.parse_args(["--count", "42", "--rate", "3.14", "--format", "json"])
    assert args.count == 42
    assert args.rate == 3.14
    assert args.format == "json"
    print("✅ 类型验证测试通过")

def test_subparsers():
    """子命令测试"""
    parser = argparse.ArgumentParser()
    sub = parser.add_subparsers(dest="command")
    init_p = sub.add_parser("init")
    init_p.add_argument("--name", required=True)
    sync_p = sub.add_parser("sync")
    sync_p.add_argument("--dry-run", action="store_true")

    args = parser.parse_args(["init", "--name", "myproject"])
    assert args.command == "init"
    assert args.name == "myproject"

    args2 = parser.parse_args(["sync", "--dry-run"])
    assert args2.command == "sync"
    assert args2.dry_run == True
    print("✅ 子命令测试通过")

def test_custom_type():
    """自定义类型测试"""
    def positive_int(v):
        iv = int(v)
        if iv <= 0:
            raise argparse.ArgumentTypeError(f"{v} 不是正整数")
        return iv

    parser = argparse.ArgumentParser()
    parser.add_argument("--limit", type=positive_int, default=10)
    args = parser.parse_args(["--limit", "42"])
    assert args.limit == 42
    print("✅ 自定义类型测试通过")

if __name__ == "__main__":
    test_basic_parse()
    test_type_validation()
    test_subparsers()
    test_custom_type()
    print("\n🎉 第13课全部验证通过!")
✅ 基本解析测试通过 ✅ 类型验证测试通过 ✅ 子命令测试通过 ✅ 自定义类型测试通过 🎉 第13课全部验证通过!

🔑 本课要点

  1. argparse > sys.argv——自动生成帮助、类型转换、错误提示
  2. 自定义 type 函数——参数验证的优雅方式
  3. subparsers——子命令让 CLI 工具可扩展
  4. mutually_exclusive_group——互斥参数(如 --json vs --yaml)
  5. action="count"——-vvv 详细度递增