Skip to content
返回项目教程

命令行工具

用 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 补全;文档写清一行安装

完整开发者生命周期

从想法到维护的 14 阶段开发者路径图 — 可视化依赖关系、追踪进度。

下一步