2. 子命令路由设计

子命令路由设计 学习卡片

为什么需要子命令路由

当 CLI 工具从单一功能扩展到多个功能时,把所有参数都塞进一个命令会让解析逻辑迅速失控。子命令路由的核心思路是:第一层只负责识别“要执行哪个动作”,第二层才解析该动作自己的参数。这样每个子命令的参数空间彼此隔离,帮助信息也能按命令粒度输出。

本节实现一个 task 工具,支持三个子命令:add(新增任务)、list(列出任务)、done(标记完成)。重点在于分发结构,以及当用户输入未知子命令时给出可操作的报错。

路由结构设计

路由表用字典把子命令名映射到处理函数,主函数只做三件事:取第一个位置参数、查表、调用。未知命令不抛裸异常,而是打印可用命令列表并以非零码退出。

argv 输入 task add "写文档" 取 argv[1] cmd = "add" 查路由表 ROUTES.get(cmd) 命中 → 调用 handler 未命中 → 友好报错 未知命令: "lst" 可用: add / list / done exit code = 2 ROUTES 表 add → cmd_add list → cmd_list

完整实现

import sys

TASKS = []

def cmd_add(args):
    if not args:
        print("用法: task add <描述>", file=sys.stderr)
        return 2
    TASKS.append({"desc": args[0], "done": False})
    print(f"已添加: {args[0]}")
    return 0

def cmd_list(args):
    if not TASKS:
        print("暂无任务")
        return 0
    for i, t in enumerate(TASKS, 1):
        mark = "x" if t["done"] else " "
        print(f"[{mark}] {i}. {t['desc']}")
    return 0

def cmd_done(args):
    if not args or not args[0].isdigit():
        print("用法: task done <编号>", file=sys.stderr)
        return 2
    idx = int(args[0]) - 1
    if idx < 0 or idx >= len(TASKS):
        print(f"编号超出范围: {args[0]}", file=sys.stderr)
        return 2
    TASKS[idx]["done"] = True
    print(f"已完成: {TASKS[idx]['desc']}")
    return 0

ROUTES = {
    "add": cmd_add,
    "list": cmd_list,
    "done": cmd_done,
}

def main(argv):
    if len(argv) < 2:
        print("用法: task <add|list|done> [参数]", file=sys.stderr)
        return 2
    cmd = argv[1]
    handler = ROUTES.get(cmd)
    if handler is None:
        print(f"未知命令: {cmd}", file=sys.stderr)
        print(f"可用命令: {' / '.join(ROUTES)}", file=sys.stderr)
        return 2
    return handler(argv[2:])

if __name__ == "__main__":
    sys.exit(main(sys.argv))

关键设计点

  • 路由表与处理函数分离:ROUTES 只做名称到函数的映射,新增子命令只需加一行,主函数无需改动。
  • 参数切片:argv[2:] 把子命令之后的参数交给对应 handler,各 handler 只关心自己的参数格式。
  • 退出码约定:成功返回 0,用法错误或未知命令返回 2,便于脚本判断。
  • 报错信息可操作:未知命令时同时打印错误命令和可用命令列表,用户无需再查帮助。

验证方式

输入预期输出退出码
task add "写文档"已添加: 写文档0
task list[ ] 1. 写文档0
task done 1已完成: 写文档0
task lst未知命令: lst / 可用命令: add / list / done2
task用法: task <add|list|done> [参数]2
如果后续子命令数量增多,可以把 ROUTES 改为装饰器注册模式,例如 @route("add"),让每个 handler 自己声明命令名,进一步降低主函数的维护成本。