第 5 章 · 02 命令解析与任务历史


文档摘要

第 5 章 · 02 命令解析与任务历史 本节摘要:本节讲 的两个支撑机制——命令解析与任务历史。命令解析用标准库 ,处理启动时的 / / 等参数;进入 REPL 后,命令路由则是一套简单的字符串比对( 、 )。任务历史存在 ,用纯文本逐行追加,保留最近 5 条,欢迎屏会展示其中最近 3 条。本节逐行拆 、 入口、以及 / 两个历史读写函数,讲清这套「极简但不简陋」的持久化设计。读完本节,你理解 AutoHedge 如何用几十行标准库做出可用的命令路由与跨会话记忆。 内容来源:原项目源码 (argparse 段第 189-214 行、main 第 217-226 行、历史读写第 47-72 行),精读并套用体系化模板。 学习目标 阅读完本节,你应当能够: 逐行读懂 与 的入口逻辑。

第 5 章 · 02 命令解析与任务历史

本节摘要:本节讲 cli.py 的两个支撑机制——命令解析任务历史。命令解析用标准库 argparse,处理启动时的 --version/--help/help 等参数;进入 REPL 后,命令路由则是一套简单的字符串比对(quit/exit/qhelp/?/h)。任务历史存在 ~/.autohedge/recent_tasks.txt,用纯文本逐行追加,保留最近 5 条,欢迎屏会展示其中最近 3 条。本节逐行拆 _build_parser()main() 入口、以及 _get_recent_tasks() / _append_recent() 两个历史读写函数,讲清这套「极简但不简陋」的持久化设计。读完本节,你理解 AutoHedge 如何用几十行标准库做出可用的命令路由与跨会话记忆。

内容来源:原项目源码 autohedge/cli.py(argparse 段第 189-214 行、main 第 217-226 行、历史读写第 47-72 行),精读并套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 逐行读懂 _build_parser()main() 的入口逻辑。
  2. 说清 argparse--version/--help 如何自动处理
  3. 理解为何 autohedge help特判--help 不用。
  4. 逐行读懂 _get_recent_tasks()_append_recent()
  5. 解释「文本逐行 + 截尾 5 条」这种极简持久化的取舍。

一、两套命令路由:启动期与 REPL 期

AutoHedge 的「命令解析」其实分两层,容易混淆,先理清:

时机 机制 处理对象 代码
启动期(终端敲 autohedge ...) argparse --version--help、bare help _build_parser() + main()
REPL 期(进入循环后的输入) 字符串比对 quit/exit/qhelp/?/h、任务 run_repl()(上一节讲过)

本节聚焦启动期(argparse)。REPL 期的路由在上一节 run_repl 已讲,核心就是 if lower in ("quit", ...) 这套简单比对——没用 argparse,因为 REPL 内的命令太简单,不值得背一个解析器。

二、_build_parser():argparse 配置

cli.py 第 189-214 行:

def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="autohedge", description="AutoHedge — interactive REPL for running research and hedging tasks.", epilog=""" Commands (when running the REPL): <task> Run a task (e.g. 'Analyze NVDA for 50k allocation') help, ?, h Show in-REPL tips quit, exit, q Exit the REPL Examples: autohedge Start the interactive REPL autohedge help Show this help autohedge --help Show this help autohedge --version Show version """, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--version", "-v", action="version", version=f"%(prog)s {VERSION}", help="Show program version and exit.", ) return parser

逐段解读:

ArgumentParser 构造

  • prog="autohedge":命令名(帮助文本里显示用)。
  • description:一句话描述(出现在 --help 顶部)。
  • epilog:长文本,出现在 --help 底部——列了 REPL 内命令、示例。注意它同时写了 autohedge helpautohedge --help,这是因为作者特判了 bare help(见下文)。
  • formatter_class=argparse.RawDescriptionHelpFormatter:保留 epilog 的原始换行(不被 argparse 自动重排),让示例的缩进对齐不被破坏。

add_argument("--version")

parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {VERSION}", ...)
  • "--version""-v" 是长短两种写法。
  • action="version":argparse 的内置动作——遇到 --version/-v,自动打印 version 字符串后直接 sys.exit()%(prog)s 会被替换成 progautohedge,所以输出形如 autohedge 0.1.5
  • 注意:这里没有 add_argument("task", ...)——也就是说 argparse 不接受位置参数任务。任务只能进 REPL 后输入。

💡 核心心法:argparse 的内置 action="version"--help(默认就有)是「自退出」动作——它们被触发后 argparse 自己调 sys.exit(),不会返回到你的代码。所以调用方不需要处理这两种情况,只管 parser.parse_args() 后继续走主逻辑即可。

三、main():入口函数

cli.py 第 217-226 行:

def main() -> None: """Entry point for the AutoHedge CLI.""" parser = _build_parser() # Treat bare "help" as --help (e.g. "autohedge help") if len(sys.argv) == 2 and sys.argv[1].lower() == "help": parser.print_help() sys.exit(0) parser.parse_args() # exits on --help / --version run_repl() sys.exit(0)

逐行解读:

特判 bare "help"

if len(sys.argv) == 2 and sys.argv[1].lower() == "help": parser.print_help() sys.exit(0)

sys.argv 是命令行参数列表(argv[0] 是程序名)。这句判断「如果用户敲的是 autohedge help(恰好一个参数,且是 help)」,就打印帮助并退出。

为什么要特判? 因为 argparse 默认只认 --help(双横线),不认 bare help。但用户的直觉是 autohedge help(像 git 那样),不是 autohedge --help。作者用一行特判满足直觉。注意它不区分大小写(.lower()),所以 autohedge HELP 也行。

parse_args 与 run_repl

parser.parse_args() # exits on --help / --version run_repl() sys.exit(0)
  • parser.parse_args():解析参数。如果用户敲了 --help/--version,argparse 内部会 sys.exit(),不会执行到这里(注释明说)。所以这行的隐含语义是:「能走到这里,说明没要 help/version,直接进 REPL」。
  • run_repl():进 REPL 循环(上一节讲)。
  • sys.exit(0):REPL 退出后,以 0(成功)退出进程。实际上 run_repl 本身不退出进程,这行保证进程干净收尾。

最后看入口注册(第 229-230 行):

if __name__ == "__main__": main()

直接 python -m autohedge.clipython cli.py 时触发。但正常用户不会这么敲——他们敲 autohedge,由 pyproject.toml[tool.poetry.scripts] 注册(autohedge = "autohedge.cli:main")直接调 main()

四、任务历史:文件位置与常量

cli.py 第 47-48 行:

RECENT_FILE = Path.home() / ".autohedge" / "recent_tasks.txt" MAX_RECENT = 5
  • Path.home():跨平台的用户主目录(Windows 是 C:\Users\用户名,macOS/Linux 是 /home/用户名/Users/...)。
  • ~/.autohedge/recent_tasks.txt:任务历史文件。注意是点开头目录(.autohedge)——类 Unix 风格的「隐藏」配置目录(虽然 Windows 不靠点隐藏)。
  • MAX_RECENT = 5:最多保留 5 条。欢迎屏只展示其中最近 3 条(上一节 _welcomerecent[-3:])。

这个位置选择有讲究:放在用户主目录而非项目目录,意味着历史是跨项目/跨会话的——你在 A 项目跑的任务,B 项目也能看到。这是「用户级」记忆,与项目代码解耦。

五、_get_recent_tasks():读历史

cli.py 第 51-60 行:

def _get_recent_tasks() -> list[str]: if not RECENT_FILE.exists(): return [] try: lines = RECENT_FILE.read_text().strip().splitlines() return [ ln.strip() for ln in lines[-MAX_RECENT:] if ln.strip() ] except Exception: return []

逐行解读:

  • if not RECENT_FILE.exists(): return []:文件不存在(首次运行)直接返回空列表,不报错。
  • read_text().strip().splitlines():整个文件读成字符串,去首尾空白,按行切分成列表。
  • lines[-MAX_RECENT:]:取最后 5 行(最近 5 条)。
  • ln.strip() for ln in ... if ln.strip():每行去空白,跳过空行(防脏数据)。
  • except Exception: return []:任何 IO 异常(权限、编码等)都吞掉,返回空列表——绝不让历史读取搞崩欢迎屏

这是一个防御性极强的函数:文件不存在、读失败、内容损坏,都优雅降级为「无历史」。门面层的健壮性体现。

⚠️ 注意 list[str] 类型注解:它用 Python 3.10+ 的内置泛型语法(list[str] 而非 List[str])。这就是为什么 pyproject.toml 要求 python = "^3.10"——3.9 不支持这种写法。

六、_append_recent():写历史

cli.py 第 63-72 行:

def _append_recent(task: str) -> None: try: RECENT_FILE.parent.mkdir(parents=True, exist_ok=True) recent = _get_recent_tasks() if task in recent: recent.remove(task) recent.append(task) RECENT_FILE.write_text("\n".join(recent[-MAX_RECENT:])) except Exception: pass

逐行解读:

  • RECENT_FILE.parent.mkdir(parents=True, exist_ok=True):创建 ~/.autohedge/ 目录(含父目录,已存在不报错)。
  • recent = _get_recent_tasks():先读出已有历史。
  • if task in recent: recent.remove(task):去重——如果这个任务已在历史里,先删掉旧的。
  • recent.append(task):把新任务追加到末尾(末尾 = 最新)。
  • "\n".join(recent[-MAX_RECENT:]):取最后 5 条,用换行符拼成字符串,整体覆盖写入(write_text 是覆盖,不是追加)。
  • except Exception: pass:任何异常静默吞掉——历史写失败不影响任务执行。

注意一个反直觉点:函数名叫 _append_recent(追加),但实际是「读 → 去重 → 改列表 → 覆盖写」。这种「读改写」模式保证了去重与截断,代价是不是真正的 append(每次都重写整个文件)。对 5 条小文本,这点开销可忽略。

💡 核心心法:这种「文本逐行 + 覆盖写 + 截尾 N 条」是极简持久化的经典套路——不用数据库、不用 JSON,一个 txt 文件搞定。适合数据量小、结构简单的场景(如最近任务、最近打开文件)。代价是没有索引、没有并发保护,数据量大或要并发写就得换 SQLite。

七、历史在欢迎屏与任务执行中的串联

把本节的两块与上一节串起来,完整流程:

启动 autohedge └─ main() → parser.parse_args() → run_repl() └─ _welcome() └─ _get_recent_tasks() ← 读历史,展示最近 3 条 └─ while True: 接收输入 └─ 用户输入任务 └─ _append_recent(task) ← 写历史(去重 + 截尾 5) └─ AutoHedge().run(task) ← 执行(下一节)

历史是跨会话的:你今天跑的任务,明天再开 autohedge,欢迎屏「Recent activity」里还能看到。这是 AutoHedge 唯一的「记忆」——Agent 本身没有跨会话记忆,只有这个用户级的任务列表。

本节要点回顾

  1. 两层路由:启动期用 argparse(--version/--help/bare help),REPL 期用字符串比对(quit/help/任务);REPL 命令太简单没用 argparse。
  2. _build_parser:ArgumentParser(prog, description, epilog, RawDescriptionHelpFormatter);--version/-vaction="version" 自退出;不接受位置参数任务。
  3. bare help 特判:sys.argv[1].lower()=="help"print_help 后退出——满足用户 autohedge help(像 git)的直觉;--help 由 argparse 自动处理。
  4. main() 三步:parse_args()(自退出 help/version)→ run_repl()sys.exit(0);由 pyproject.scripts 注册为 autohedge 命令。
  5. 历史文件:~/.autohedge/recent_tasks.txt,MAX_RECENT=5,欢迎屏展示最近 3;放用户主目录=跨项目跨会话。
  6. _get_recent_tasks:防御性极强——文件不存在/读失败/内容损坏都优雅降级为空列表,不崩欢迎屏。
  7. _append_recent:「读 → 去重 → 改列表 → 覆盖写」(名 append 实 rewrite);异常静默吞掉不影响任务;极简持久化适合小数据。

下一节,我们看 REPL 背后的核心——autohedge/main.py 的 AutoHedge 主类,以及它支持的三种输出格式(list/dict/str)。


发布者: 作者: 灏天文库 转发
评论区 (0)
U