命令行工具
用 Node.js、Go 或 Rust 构建可分发的 CLI,通过 npm/brew/cargo 发布。
设计与接口
定义命令、标志、输出格式,以及优秀终端工具的用户体验。
为什么
CLI 设计是文本中的 UX 问题。好 CLI 可发现(--help)、一致(标志到处可用)、可脚本化(稳定输出、退出码)。设计差的 CLI 被人绕过并弃用;优秀的(如 ripgrep)成为日常习惯。接口即产品。
何时使用
实现之前。1-3 天敲定命令、标志和输出格式。先画 --help 输出。
如何做
1. 选择语言
- Node.js:发布最快,经 npm 分发(npx your-cli)
- Go:单一静态二进制,经 brew/release 分发(无运行时依赖)
- Rust:快 + 单一二进制,经 cargo + release 分发
2. 设计命令和标志
- 子命令:mycli build、mycli deploy(非 mycli-build)
- 标志:--flag(长)、-f(短),跨子命令一致
- 全局标志:--verbose、--config、--help、--version
- 用框架:commander(Node)、cobra(Go)、clap(Rust)
3. 设计输出
- 默认:人类可读,TTY 时上色(尊重 NO_COLOR)
- --json 或 --output json 供机器消费
- 错误到 stderr,结果到 stdout(可管道)
- 退出码:0 成功,1 通用错误,2 用法错误
4. 设计配置 + 状态
- 配置:~/.config/mycli/config.toml(XDG 基础目录规范)
- CI 用环境变量:MYCLI_API_KEY、MYCLI_CONFIG
- 优先级:标志 > 环境变量 > 配置文件 > 默认
5. 画 help
- 编码前先写 --help 输出——它就是规格
- 长描述中包含示例
- 自动生成补全(bash/zsh/fish)
示例
先画 help(即规格):
mycli - 从终端部署静态站点
用法:
mycli <命令> [标志]
命令:
deploy [目录] 部署目录(默认:.)
init 创建 .myclirc 配置
logs [数量] 显示最近部署日志
标志:
--prod 部署到生产(默认:预览)
--json 输出 JSON 供脚本
-v, --verbose 详细日志
示例:
mycli deploy --prod
mycli logs 20 --json | jq '.[] | .url'
框架:clap(Rust)从 struct 属性自动生成
常见问题
- ⚠子命令间标志不一致(--prod 在 deploy 可用但 logs 不可用)——破坏肌肉记忆
- ⚠上色输出破坏管道——始终检测 TTY 并尊重 NO_COLOR
- ⚠无机器可读输出——用户只能 grep/awk 人类文本,脆弱
最佳实践
- ✓编码前先写 --help——它迫使你提前设计完整接口
- ✓错误始终到 stderr,结果到 stdout,让工具在 Unix 管道中可组合
- ✓第一天就提供 --json 输出;脚本用户会感激且永不离开
实现与测试
用框架构建 CLI,测试标志/输出,并产出发布构建。
为什么
CLI 在管道和 CI 中运行——成功时非零退出码,或该输出 JSON 却输出人类文本,会破坏下游自动化。测试标志解析、退出码和输出格式,能在用户脚本绕过并锁定 bug 行为前发现它们。
何时使用
接口设计后。先实现正常路径,再边界情况(缺参数、无效配置、网络失败)。
如何做
1. 用框架脚手架
- Node:commander + chalk(大规模多命令用 oclif)
- Go:cobra + viper(配置)、zerolog(日志)
- Rust:clap + anyhow(错误)、tracing(日志)
2. 增量实现命令
- 一个子命令一个 PR;共享核心库,不复制粘贴
- 早验证参数,快失败并给清晰用法错误(exit 2)
- 长时操作:显示 spinner(ora/indicatif),尊重 --quiet
3. 处理错误和退出码
- 用法错误 -> exit 2,打印 --help 提示
- 运行时错误(网络、配置)-> exit 1,清晰信息到 stderr
- 成功 -> exit 0,绝不输出到 stderr
4. 测试 CLI
- 参数解析和核心逻辑的单元测试
- 集成测试:运行二进制,断言 stdout/stderr/退出码
- 快照测试 --help 输出,捕捉意外接口变更
5. 为分发构建
- Node:发布 ESM + CJS,package.json 设 "bin"
- Go:GOOS/GOARCH 矩阵 -> 静态二进制(无运行时依赖)
- Rust:cargo build --release -> 优化二进制
示例
Rust + clap 命令:
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Deploy { dir: Option<PathBuf>, #[arg(long)] prod: bool },
Logs { limit: Option<u8> },
}
集成测试(可断言):
let out = Command::new("./mycli").args(["logs","1","--json"]).output()?;
assert_eq!(out.status.code(), Some(0));
let v: Vec<Log> = serde_json::from_slice(&out.stdout)?;
CI 矩阵构建(Go):
go build -o dist/mycli-$GOOS-$GOARCH .
常见问题
- ⚠错误时返回 exit 0——破坏依赖非零退出码失败的 CI 步骤
- ⚠错误打印到 stdout——破坏下游解析器的 --json 输出
- ⚠二进制无集成测试——单元测试过但构建的 CLI 标志坏了
最佳实践
- ✓测试构建的二进制,而非仅库函数——运行它并断言退出码 + 输出
- ✓快照 --help 输出,让任何接口变更都是有意且经审查的 diff
- ✓CI 中建 GOOS/GOARCH(或 target triple)矩阵,每次发布全平台
分发与发布
经 npm/brew/cargo 发布,并用变更日志自动化版本发布。
为什么
分发摩擦会杀死 CLI——安装超过一条命令,采用就停滞。npm 给 npx 一行安装;brew tap 触达 macOS 开发者;cargo install 触达 Rustacean。自动化签名发布加变更日志,把'我更新了吗?'变成已解问题。
何时使用
CLI 在所有目标平台构建并测试通过时。v1.0 前就建好发布流水线,让首次公开发布自动化。
如何做
1. 选择分发渠道
- npm:"bin" 字段 + npx your-cli(无需安装)
- Homebrew:创建 tap,brew install your-tap/your-cli
- cargo:发布到 crates.io,cargo install your-cli
- GitHub Releases:每个 OS/arch 的静态二进制
2. 自动化发布
- 语义化版本:打 tag v1.2.0 -> CI 构建 + 发布
- release-please 或 release-plz 升版本 + 生成变更日志
- 把签名二进制附到 GitHub Release
3. 生成变更日志
- 约定式提交 -> 自动生成 CHANGELOG.md
- 按 feat/fix/breaking 分组,用户快速扫升级
- 每条链接 PR/提交供上下文
4. 发布 shell 补全
- 从框架生成 bash/zsh/fish 补全
- 文档安装:"source <(mycli completions zsh)"
- Homebrew formula 可自动安装补全
5. 宣布和支持
- v1.0 发到 HN、相关 subreddit、语言论坛
- 置顶一个 issue 收 v1.0 反馈;分拣进变更日志
- 从开始有破坏性变更就维护"迁移到 v2"文档
示例
npm(package.json):
{ "name": "mycli", "bin": { "mycli": "./dist/cli.js" } }
# 用户运行:npx mycli deploy --prod(无需安装)
GitHub Actions 发布(打 tag 时):
- go build 矩阵(linux/darwin/windows x amd64/arm64)
- gh release create v1.2.0 dist/* --notes-from-tag
- npm publish(Node)/ cargo publish(Rust)
Homebrew tap(homebrew-tap/mycli.rb):
class Mycli < Formula
url "https://github.com/me/mycli/releases/download/v1.2.0/mycli-darwin-arm64"
sha256 "..."
def install; bin.install "mycli-darwin-arm64" => "mycli"; end
end
常见问题
- ⚠手动发布——漏平台、版本升错、无变更日志,用户卡在旧版
- ⚠无 shell 补全——重度用户 tab 补全无果,转用竞品 CLI
- ⚠破坏性变更无迁移文档——用户升级后崩坏,再也不更新
最佳实践
- ✓打 git tag 自动发布——一条命令同时发 npm + brew + cargo + GitHub 二进制
- ✓每次发布生成并附带 shell 补全;文档写清一行安装