如果你和我一样,每天要在终端里敲几十遍几乎相同的命令,迟早会产生一个念头:能不能把所有这些操作统一成一条命令?我最近用一周多的业余时间折腾了一个叫 CLI-Anything 的小项目,灵感很简单——Anything 都能成为 CLI。这个项目把重复的 Git 操作、Docker 清理、目录切换,甚至对 Codex CLI 和 Claude CLI 的调用,全部收编到了同一个入口里。它不是某个大厂的正式产品,而是适合所有终端使用者的个人脚手架。无论你是被命令行劝退过的新手,还是已经写了多年脚本的老手,都可以从里面抄走一部分设计,或者直接把它 clone 下来改造成自己的工具箱。今天这篇就把我的设计思路、完整实现、踩过的坑一次说清楚。
1. 为什么会有 CLI-Anything:从“记命令”到“造命令”
1.1 一切皆命令的灵感来源
CLI 的本质其实非常简单:一个命令就是一个可复用的输入输出转换。你给它参数,它产生结果。我之所以想到做 CLI-Anything,是因为我发现自己在终端里的日常可以被拆成三件事:查状态、改东西、跑流程。这三件事的命令参数通常又长又容易记错。
打个比方,这就像厨房里的一堆调料。做一次菜,要临时想放几克盐、几勺生抽、什么时候下锅,每次都要重新配比。如果你把常用的做法做成固定调料包,做菜时就省掉大量重复决策。命令行也一样:复杂操作封装成一条短命令,以后只面对那个短命令,而不用面对背后的二十个参数。
命令行本身已经提供了这三件事的原子积木:git负责版本状态、docker负责容器、curl负责网络请求、jq负责解析 JSON。但它们彼此之间没有“胶水”,每次组合都要重新写一遍。CLI-Anything 就是这一层胶水。
1.2 它到底解决什么问题
往小了说,CLI-Anything 解决四类问题:
- 重复操作:每天都要执行
git add -A && git commit -m "xxx" && git push,为什么不直接敲ggpush "xxx"? - 记忆成本:
ffmpeg转码、rsync同步这类命令参数极多,真正用到时还要翻历史记录。 - 跨工具调用:查完日志还要转去另一个工具跑数据分析,命令之间互不相通。
- 团队复用:你自己磨合出来的命令,为什么不能一键分享给同事?
这些问题的共通点,是把“人的经验”沉淀成“可执行文件”。CLI-Anything 提供一个统一的入口、统一的帮助信息、统一的错误返回码,让沉淀下来的脚本不再是散落各处的.sh文件,而是一个“工具箱”。对我个人来说,它最大的价值是“卸载记忆负担”:我不需要记住某个操作的具体参数,只需要记得这个操作在工具箱里叫什么名字。
2. 核心设计:一个“什么都能长出来”的命令骨架
2.1 目录结构与命令注册机制
CLI-Anything 的骨架非常轻,核心思路是“一个子命令对应一个可执行文件”。这种设计在 Go 的 cobra、Rust 的 clap 里很常见,但我们不需要引入多复杂的框架,一个目录加一个小脚本就能实现。
项目结构大概是这样的:
cli-anything/ ├── bin/ │ └── cli-anything # 统一入口脚本 ├── commands/ │ ├── ai-commit.sh │ ├── docker-clean.sh │ ├── weekly-report.sh │ └── ... ├── lib/ │ └── helpers.sh # 公共函数 ├── config.env # 全局配置 └── README.md入口脚本只做一件事:从commands/目录里找到与第一个参数同名的文件,然后执行它。如果找不到,就打印帮助信息。
#!/usr/bin/env bash # bin/cli-anything set -euo pipefail COMMAND_NAME="${1:-}" shift 2>/dev/null || true if [[ -z "$COMMAND_NAME" ]]; then echo "Usage: cli-anything <command> [args...]" echo echo "Available commands:" for f in commands/*.sh; do name=$(basename "$f" .sh) desc=$(head -1 "$f" | sed 's/^# //') printf " %-24s %s\n" "$name" "$desc" done exit 0 fi SCRIPT="commands/${COMMAND_NAME}.sh" if [[ ! -f "$SCRIPT" ]]; then echo "error: unknown command '${COMMAND_NAME}'" >&2 echo "Run 'cli-anything' to see available commands." >&2 exit 1 fi # 让子命令可以访问公共函数和配置 source lib/helpers.sh source config.env exec bash "$SCRIPT" "$@"这段脚本我用了set -euo pipefail。-e保证任何一步出错就停下来,-u避免用到未定义变量,pipefail防止管道中的错误被吞掉。这算是 shell 脚本的三件套,很多人不写,等到出问题才后悔。唯一要注意的是,如果你确实需要某个命令“即使失败也要继续”,可以在子命令里临时关闭它。
2.2 参数解析、帮助信息与错误处理
每个子命令我都强制要求两件事:必须能打印帮助信息,必须用非零退出码表示失败。这两条规矩让整个工具箱的体验非常一致。
以docker-clean.sh为例:
#!/usr/bin/env bash # 清理无用的 Docker 容器、镜像和构建缓存 set -euo pipefail usage() { echo "Usage: cli-anything docker-clean [--force]" echo " --force 跳过确认,直接清理" } while [[ $# -gt 0 ]]; do case "$1" in --help|-h) usage exit 0 ;; --force) FORCE=1 shift ;; *) echo "error: unknown option: $1" >&2 usage >&2 exit 1 ;; esac done if [[ "${FORCE:-0}" != "1" ]]; then read -r -p "确认清理所有无用的数据? [y/N] " confirm if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then echo "已取消" exit 0 fi fi docker system prune -af --volumes这个脚本本身不复杂,但有几个细节值得说。第一个是read -r,-r避免反斜杠被转义。第二个是默认交互式确认,只有加--force才跳过,这是防止手滑的保命设计。第三个是错误信息全部写到标准错误(>&2),不然在管道里排查问题时会特别痛苦。
2.3 把“任何东西”变成子命令的三种打法
CLI-Anything 之所以叫 Anything,是因为往commands/里丢一个可执行文件,它就是一个新命令。我总结下来,子命令常见的来源有三种。
第一种是直接包装,把一段你复制粘贴了无数次的命令变成脚本。比如“一键提交代码”:
#!/usr/bin/env bash # 一键 add、commit、push set -euo pipefail git add -A git commit -m "${1:-chore: update}" git push第二种是组合多个已有 CLI。命令行工具的强处是每个都很专注,弱处是组合起来繁琐。你可以用 CLI-Anything 把“用户态工具链检查”这种流程串起来:
#!/usr/bin/env bash # 检查开发环境是否就绪 set -euo pipefail for cmd in git node go docker; do if command -v "$cmd" >/dev/null 2>&1; then echo "OK $cmd" else echo "MISS $cmd" fi done第三种是调用 API,这通常需要写一点 Python。比如一份脚本把内部接口的数据拉到本地生成表格。这三种打法交替使用,基本能覆盖日常 90% 的重复工作。
3. 接入 AI CLI:把 Codex CLI 和 Claude CLI 收编进命令箱
3.1 安装 Codex CLI:从 npm 到二进制
最近 AI 编程助手在终端里越来越火,Codex CLI 就是 OpenAI 官方出的命令行工具。它可以直接在终端里跑对话、生成代码、执行任务。我把它作为 CLI-Anything 的一个重要子命令来源,因为“调用 AI”本身也是一件重复的事。
安装 Codex CLI 最常规的方式是 npm:
npm install -g @openai/codex装完先验证版本:
codex --version如果能看到版本号,说明安装成功。之后可以直接交互式启动:
codex也可以非交互式给一个任务:
codex "查看当前目录的项目结构,并用树状图列出来"我在实际使用中会把一些固定任务交给它,比如“帮我的 commit message 起个标题”“给这段代码写单元测试”。这些任务本身很简单,但每次都要描述一遍,也很费精力。所以我在 CLI-Anything 里加了一个ai子命令,后面详细说。
安装时有一个高频错误:unable to locate the codex cli binary or required runtime components。我在第 5 节会专门讲排查思路,这里先说结论,八成是 PATH 没有把 Codex CLI 的安装目录包含进去,或者 Node.js 版本太低。
3.2 配置 Claude CLI:Mac 上如何切换模型 Key
Claude CLI 是另一款非常好用的 AI 终端工具,它默认读取ANTHROPIC_API_KEY这个环境变量作为凭据。如果你有自己的 Claude API Key,直接在 shell 里 export 就行:
export ANTHROPIC_API_KEY="sk-ant-..."很多朋友在 Mac 上使用 Claude CLI 时,想的不是用官方的模型,而是把手上的其他模型 Key 接进去。比如我有一次想把 Qwen Key 配到 Claude CLI 里,因为同样一套交互界面,可以切到底层模型不同。社区里常见的做法是显式指定兼容的接口地址和模型名:
export ANTHROPIC_API_KEY="sk-你的-qwen-key" export ANTHROPIC_BASE_URL="https://你的兼容接口地址" export ANTHROPIC_MODEL="qwen-max"配置完以后,启动claude时它就会用你指定的 Key 和模型。这里有个细节需要提醒:环境变量的名字要区分大小写,anthropic_api_key是无效的。我见过不止一次因为环境变量名小写了,结果 CLI 像没带钱就进超市,直接报错。
更推荐的做法是把它写进~/.zshrc,但不要把密钥直接硬编码进脚本仓库。可以单独放到一个不在 Git 仓库里的配置文件,然后用source加载。
3.3 在 CLI-Anything 里统一调度 AI 命令
有了 Codex CLI 和 Claude CLI 之后,下一步就是把它们塞进 CLI-Anything。我建了一个ai子命令,用来区分调用哪个 AI 工具。
#!/usr/bin/env bash # 统一 AI 命令入口 set -euo pipefail TASK="${1:-}" if [[ -z "$TASK" ]]; then echo "Usage: cli-anything ai <commit|review|weekly-report|chat>" exit 1 fi case "$TASK" in commit) codex "根据 git diff 生成一条符合 conventional commits 规范的 commit message" ;; review) claude "请对当前分支的改动做一次 code review,指出风险和优化点" ;; weekly-report) claude "根据 git log 生成周报" ;; *) echo "error: unknown ai task '$TASK'" >&2 exit 1 ;; esac这样做的价值在于,团队内部不用每个人都去记住“这个任务找 Codex,那个任务找 Claude”,只需要记cli-anything ai 什么什么。工具是可以替换的,但入口保持一致。
4. 从零实现一个真实子命令:一键生成周报
4.1 先定义输入和输出
空谈设计容易飘,我拿一个真实子命令完整走一遍。目标是做一个weekly-report命令:基于 Git 提交记录自动生成一周工作总结。
先定义输入:
- 日期范围的起始日期和结束日期
- 可选:使用哪个 AI CLI(默认 claude)
- 可选:输出文件路径
再定义输出:
- 一份 Markdown 格式的周报,包含本周提交统计、主要改动模块、AI 生成的总结段落
4.2 写脚本并接入 CLI-Anything
第一步是拿到提交记录。git log本身就能筛选日期范围和提交信息:
git log --since="$start" --until="$end" --pretty=format:"%h %s"为了让 AI 生成更自然的总结,我把提交信息拼成一段有序列表,再交给 Claude CLI 做整理。脚本如下:
#!/usr/bin/env bash # 基于 git log 生成周报 set -euo pipefail START="${1:-$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d '7 days ago' +%Y-%m-%d)}" END="${2:-$(date +%Y-%m-%d)}" OUT="${3:-weekly-report-${END}.md}" LOG_CONTENT=$(git log --since="$START" --until="$END" \ --pretty=format:"- %s" --no-merges) if [[ -z "$LOG_CONTENT" ]]; then echo "warning: 没有找到提交记录,生成空周报" >&2 LOG_CONTENT="- 本周无提交" fi { echo "# 周报 ${START} 到 ${END}" echo echo "## 本周提交记录" echo echo "$LOG_CONTENT" echo echo "## AI 总结" echo } > "$OUT" claude "以下是 ${START} 到 ${END} 的提交记录,请用三句话总结本周工作重点,并给出下周建议: $LOG_CONTENT" >> "$OUT" echo "周报已生成:$OUT"这个脚本有几个值得注意的处理。一是日期默认值的兼容写法:date -v-7d是 macOS 的 BSD date 语法,date -d '7 days ago'是 Linux 的 GNU date 语法。我在脚本里用||做了双保险,这样同一个脚本在 Mac 和 Linux 上都能跑。二是--no-merges过滤掉合并提交,避免周报里全是 merge 噪音。三是即使没有提交记录,也要生成空周报而不是直接报错。
4.3 参数计算的完整过程
很多 AI 调用会按 token 计费,所以在交给模型之前,我会先估算一下文本量和费用,避免一条命令烧掉不必要的额度。
一个粗略的估算公式:token 数 ≈ 字符数 / 4。英文大概 4 个字符一个 token,中文会高一些,粗略按 1 到 1.5 个字符一个 token 算更保守。我在脚本里加了一段提示:
CHARS=$(echo -n "$LOG_CONTENT" | wc -m | tr -d ' ') TOKENS_EST=$((CHARS / 3)) echo "输入约 ${TOKENS_EST} tokens,费用取决于当前模型单价" >&2假设本周提交 500 个汉字,那估算大概 500 到 1600 tokens,取中间值 800。如果当前模型输入单价是每百万 tokens 20 元,那这次调用的输入成本约 0.016 元,几乎可以忽略。如果提交记录特别丰富,达到 1 万字符,那估算 3000 到 10000 tokens,成本也不过几毛钱。真正贵的是把整个仓库的 diff 都塞进去,那样动辄几十万 token,所以在设计命令时,能传摘要就不要传全文。
4.4 运行效果与结果校验
脚本放到commands/weekly-report.sh后,运行:
cli-anything weekly-report 2025-01-06 2025-01-10大约十几秒后,目录下出现weekly-report-2025-01-10.md。打开文件,前面是整理过的提交列表,后面是 AI 总结。我遇到过一个有意思的情况:某次git log里的提交信息全是“fix bug”“update”,AI 写出来的总结也特别敷衍。所以后来我给自己立了个规矩,提交信息要写清楚模块和意图,否则下游所有自动化质量都会被打折扣。
结果校验也很简单,三步:文件是否生成、AI 总结是否与提交记录一致、日期范围是否准确。有时候--until不写当天日期,会漏掉当天最后几个提交,因为git log默认按 00:00 边界算。要包含当天,建议--until="$END 23:59:59",这个小坑很隐蔽。
5. 常见问题与排查技巧实录
5.1 unable to locate the codex cli binary or required runtime components
这应该是 Codex CLI 用户遇到最多的报错之一。完整的报错类似unable to locate the codex cli binary or required runtime components. check your installation。它翻译过来就是“找不到 codex 的可执行文件,或者缺少运行环境组件”。
我排查这个问题的顺序是固定的:
- 先跑
which codex,看系统能不能找到这个命令。如果没有任何输出,说明安装目录没进 PATH。 - 再跑
npm root -g,拿到全局 node_modules 路径,检查该路径下有没有@openai/codex。 - 确认 Node.js 版本,
node -v。Codex CLI 通常对 Node 版本有要求,太低会直接跑不起来。 - 如果二进制是从压缩包解压出来的,检查文件是否有执行权限:
ls -l $(which codex)。 - 最后尝试重新安装一次,重点看安装日志末尾有没有异常。
多数情况在第 1 或第 2 步就解决问题。如果是 PATH 的问题,在~/.zshrc或~/.bashrc里加上全局 bin 目录:
export PATH="$(npm root -g)/.bin:$PATH"这是 npm 基础操作,但很多人就是因为没加这一行,卡在“装好了却用不了”的状态。
5.2 PATH 不对 / 找不到命令
这几乎是所有 CLI 工具的通病。command not found有几种情况:
- 没安装
- 安装了但目录不在 PATH 里
- 权限没给执行权限
- 你用的 shell 和安装时用的 shell 不是同一个
我有一个快速定位技巧:先type codex或which codex。如果输出了/usr/local/bin/codex,但执行还是失败,那可能是动态库或解释器的问题。如果是 npm 全局安装,可以npm list -g --depth=0看看包是否真的存在。
很多人在 Mac 上遇到“在终端里能用,在脚本里不能用”的问题,原因是 cron 或自动化脚本的环境变量少了~/.zshrc。这时要么在脚本开头显式source ~/.zshrc,要么把 PATH 硬编码进去。我的做法是统一在config.env里定义路径,CLI-Anything 入口会加载它。
5.3 Mac 上 Claude CLI 使用 Qwen Key 的配置细节
用 Qwen Key 接 Claude CLI 时,最大的坑就是“环境变量名不匹配”和“接口地址不匹配”。我建议按这个顺序核对:
| 检查项 | 正确做法 |
|---|---|
| 变量名 | ANTHROPIC_API_KEY,全部大写、下划线分割 |
| 接口地址 | ANTHROPIC_BASE_URL,必须是服务方提供的兼容地址,注意结尾不要多带路径 |
| 模型名 | ANTHROPIC_MODEL,要和服务方支持的模型 ID 完全一致 |
| Shell 加载 | 修改~/.zshrc后,执行source ~/.zshrc或重开终端 |
还有一个体验性建议:不要直接覆盖全局的ANTHROPIC_API_KEY。如果你还有别的 Claude Key,可以给不同 Key 起不同的别名:
alias claude-qwen='ANTHROPIC_API_KEY="sk-xxx" ANTHROPIC_BASE_URL="https://..." ANTHROPIC_MODEL="qwen-max" claude'这样同一个 CLI 工具可以一键切换后端,互不干扰。
5.4 网络超时、API 限额和权限问题
调用 AI CLI 时,另外两类高频问题是网络和时间。网络问题表现为请求超时、连接被拒、长时间无响应。这种时候先检查本机网络是否能正常访问目标 API,用curl -I简单试探一下;再检查 API 服务方的状态页,看是不是服务端在维护。不要每遇到超时就怀疑工具坏了,很多时候是临时的网络抖动。
API 限额问题则直接体现为 HTTP 429 或 403,说明当前 Key 的配额用完了或者权限不够。排查思路是:登录服务平台看用量、检查 key 是否还有有效期、确认请求头里的模型 ID 是否有权限。我建议在 CLI-Anything 的配置里不要把 key 写死,而是从环境变量读取,一旦 key 轮换,只需改一处。
这些看起来都像是基础设施问题,但落在日常工作中,就是每个命令都可能遇到的事。有了统一入口以后,我可以在入口脚本里加超时、日志、退出码汇总,把这样基础设施问题也收口到一个地方。
6. 后续扩展与我的心法
6.1 从“我的工具箱”到“团队工具箱”
CLI-Anything 做到这个程度,本质上已经是一个可复用的开发工具链。我之后还想做的扩展是把这些子命令推给团队使用:统一帮助、统一配置、统一错误处理,新人加入时只需要跑一条setup.sh。
还有几个我想加的玩法:
- 给所有子命令加
--json输出,方便脚本之间继续处理。 - 把耗时命令接入通知,执行完弹一个本地通知。
- 对接定时任务,周五下午自动生成周报并发到邮箱。
- 把
ai子命令的大模型参数做成可配置,让团队成员各自选模型。
这些扩展并不难,难的是保持“入口简单、子命令单一”的设计惯性。每多一个花哨功能,都要问自己一句:这个功能真的应该放在 CLI 里吗?如果答案是“偶尔需要”,那就不如不加。
6.2 我在实际使用中的几点体会
折腾 CLI-Anything 这周,我最大的体会是:先有重复,再有封装。很多人一上来就想着“我要写一个万能框架”,结果根本没想清楚解决什么问题,最后只得到一个漂亮但没有用户的项目。我的建议是,先把自己的高频操作记下来,至少连续记三天,再挑出现次数最多的那几条,写成子命令。这样每条命令都是真实需求,不是臆想。
第二个体会是:输出要比输入更友好。命令行脚本不一定非要追求全是字符,可以适当加一点颜色提示、分隔线、进度信息。我通常会在脚本里输出“命令名称、耗时、结果路径”这三样信息,让使用者在屏幕上一眼就能判断是否成功。
第三个体会是:别怕用 AI 帮忙写脚本。像这种小工具,完全可以让 Codex CLI 先给你一版,你来改边界条件。实测下来,AI 写的脚本骨架一般比手写更快,但路径处理、异常输出、兼容性这些坑,还是要靠人把关。工具负责快,人负责准,配合起来才舒服。
如果让我给一句话总结,那就是:CLI-Anything 不是一个终点,而是一个可以一直生长的项目。今天你多写一条子命令,明天你的工作就能少重复一次。把自己最常做的事情做成命令,可能是对个人效率最值得的一笔投资。