说一下我当时为什么突然想折腾 CLI。如果你最近把目光放在 AI 编程助手上,那么你多半会和"CLI"这个词打交道:Codex CLI、Claude Code CLI、GitHub CLI,甚至各种模型厂商的本地命令行入口,都在用同一套终端交互模式。更真实的是,大多数人并不会被算法卡住,而是被一个看起来很蠢的问题拦住——unable to locate the codex cli binary。聊天工具、编辑器插件、自动化脚本都在找同一个命令,却经常找不到它。这个现象背后其实就是 CLI 工具核心的"安装、寻址、调用"链路问题。我这个"高级项目:构建一个 CLI 工具",决定用一个完整的实战案例把它讲透。
我会带你从零构建一个可用的命令行批量重命名工具,命名为 brc。它既不是玩具,也不是把功能堆在 main 函数里的脚本,而是要考虑参数解析、递归扫描、重命名冲突、dry-run 预览、发布安装,以及被其他桌面应用集成时的二进制路径问题。读完你可以直接照做,也能把这些经验迁移到任何语言的 CLI 开发里。适合还没系统写过 CLI、或者老被 command not found/unable to locate xxx cli binary 折磨的人。
1. 很多人写 CLI,先输在“二进制被发现”这道关上
1.1 CLI 作为高级项目为什么值得做
命令行工具看起来不炫,却是最有复用价值的开发产物之一。它能被写成脚本调用、被 CI 系统调用、被编辑器或桌面应用嵌套调用,还能被其他开发者通过 npm、brew、apt 等工具安装。相比 GUI,CLI 的核心优势是接口稳定、输入输出可被程序解析、运行环境轻量。
我见过很多开发者能写出很复杂的后端服务,却不太会建一个规整的 CLI 项目。常见问题是:入口文件缺少 shebang 导致直接执行报错,不知道 package.json 的 bin 字段是干什么的,也不清楚安装之后命令为什么没有被放进 PATH。再加上最近很多 AI 工具都采用“桌面应用外挂一个 CLI 二进制”的架构,一旦桌面应用在启动时找不到对应二进制,就会弹出一大段类似 ChatGPT 桌面版那样的报错。所以,把 CLI 当做一个正经项目来系统做一遍,你会同时补上几块基础能力。
1.2 从一个报错入手:定位 CLI 的完整路径
先看一个活生生的案例。搜索窗里经常看到的报错原文大概是:
text复制Unable to locate the codex cli binary. Set codex_cli_path or ensure the electron resources include bin/codex.
这里点名了两条修复路径:一是设置一个叫 codex_cli_path 的环境变量,二是确保 Electron 应用的 resources 目录里包含 bin/codex。你细品一下,它其实不是告诉你“这个 CLI 坏了”,而是在说宿主程序不知道 CLI 在哪里。
在类 Unix 系统里,程序在终端中运行,会在 PATH 目录列表里逐个查找命令;而 GUI 应用从桌面启动时,往往没有加载你 .zshrc 或 .bashrc 里的环境变量,所以你在终端里执行 which codex 能输出结果,桌面应用却依然找不到。默认目录里放一个固定路径的二进制,是很多桌面应用的兜底方案。这就是为什么报错里要求“resources 里要有 bin/codex”。
这和我们自己构建 CLI 有什么关系?关系很大。一个合格 CLI 工具不能只在开发机的终端里跑,还得考虑安装之后如何出现在系统 PATH 中、如何被上层应用以绝对路径调用、如何优雅地给出路径错误提示。后面的章节我会用实际命令一步步演示,现在先把目标项目定义清楚。
1.3 本项目的目标与命令用法
我要做的 brc 是一个批量重命名工具,解决的是最日常的文件整理问题:把某个目录下所有 .md 文件中的 draft- 前缀去掉,把日志目录里的 .log 统一改成当前日期格式,或者把递归目录下所有空格替换成下划线。
它的预期用法是:
bash复制# 把 ./docs 下所有 .md 文件名中的 draft- 替换为空
brc --path ./docs --find "draft-" --replace "" --ext .md
# 先预览,不实际执行;默认就是 dry-run
# 看效果没问题后再加 --apply
brc --path ./assets --find " " --replace "_" --apply
设计上,默认只输出重命名计划,必须显式加 --apply 才真正修改磁盘文件。这能避免手滑造成不可逆事故。整个项目会用 Node.js 18+ 实现,不依赖第三方库,尽量让你在没有网络的情况下也能跑通核心逻辑。后面我会解释为什么这么选,也会说明真实场景下你可能需要 commander 之类的增强库。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前的设计:我把一个文件处理需求拆成命令行
2.1 功能边界与核心操作对象
做 CLI 最怕的就是需求无限膨胀。一开始我只想解决“把目录下的文件名批量替换”,但很快会冒出“支持正则”“支持交互式确认”“支持撤销”“支持按修改时间过滤”等一堆想法。高级项目不等于什么都做,而是把主线功能做得足够稳。
这次我先圈定几个明确的使用场景:
- 支持指定扫描目录,默认当前目录。
- 支持递归扫描子目录。
- 支持按扩展名过滤,例如只处理
.md文件。 - 支持
--find和--replace,做简单的子串替换。 - 默认跳过隐藏目录(像
.git、node_modules)。 - 默认 dry-run,显式加
--apply才执行。
为什么用子串替换而不是直接上正则?一方面场景足够清晰,另一方面新手不容易被参数转义坑到。你实际想用正则时,把实现从 replace(find, replace) 改成 replace(new RegExp(find, 'g'), replace) 并不难。
2.2 命令格式与参数解析的关键取舍
CLI 的参数解析是所有交互的入口。很多人会觉得“解析参数”很简单,其实需要提前想清楚:
- 短选项和长选项要不要都支持?例如
-p和--path。 - 布尔选项怎么区分“没传”和“传了 false”?
- 遇到无法解析的参数是直接报错还是忽略?
- 要不要支持
--help和--version?
我的决定是:短选项暂不实现,只支持长选项,因为命名语义清楚;boolean 选项只保留一个正向触发 --apply,默认不执行操作,这是安全原则。--help 和 --version 属于 CLI 标配,必须提供,否则别人拿到你的工具没法
