2948 字
约 9 分钟
17
S01-A09 · 命令行的可发现性:补全、帮助与错误提示

工具装好了,--help 写了 60 行,README 贴了三条示例。一个月后同事还是用他那套 sed | xargs 拼出来的旧脚本。这不是培训问题:命令行工具的界面只有一条窄缝——从键盘输入到屏幕输出。用户在这条缝里学习你的工具,能拿到的线索只有三种:按 Tab 出现的候选、--help 的文本、敲错时的报错。可发现性就是把这三处设计好。

真正的决策不是"要不要写帮助",而是这三处的投入怎么分配,以及什么条件下这笔投入不该花。


1. 三个入口的分工

入口 触发时机 用户此刻状态 解决的问题 实现成本
补全 敲到一半按 Tab 已决定用工具,正想参数 存在性:有什么子命令、什么取值 中到高
--help 主动索取 不确定意图能否表达 语法:互斥、默认值 低,易腐坏
错误提示 参数不合法 已失败,最想放弃 修正路径:改哪个字 低到中,最被忽略

优先级和直觉相反:错误提示收益最高,它出现在用户最想放弃的瞬间;补全成本最高,要和 shell 生态对接;--help 最好写,也最容易和实际行为脱节。

可用的配比是先让错误提示"能复制粘贴重跑",再做 --help 的示例层,最后才考虑补全。反过来做,就是花两周写补全、报错仍是 invalid argument。


2. 补全:静态脚本与动态生成

# 静态:候选写死在脚本里,随工具发布
_mycli() {
  local cur=${COMP_WORDS[COMP_CWORD]}
  case "${COMP_WORDS[1]}" in
    deploy) COMPREPLY=($(compgen -W "--env --dry-run --tag" -- "$cur")) ;;
    *)      COMPREPLY=($(compgen -W "deploy rollback status" -- "$cur")) ;;
  esac
}
complete -F _mycli mycli
# 动态:工具自己吐脚本,候选来自运行期状态
$ mycli completion bash > /etc/bash_completion.d/mycli   # 安装时
$ mycli __complete deploy --                             # 每次 Tab
_dag_complete:4: command not found: mycli                # 典型故障
维度 静态脚本 动态生成
候选来源 手写 根据上下文计算(分支名、集群名、镜像 tag)
与代码一致性 必然漂移,加参数时没人回头改 天然一致,同一份定义
首次延迟 毫秒级 每次 Tab 起子进程,冷启动可达数百毫秒
失效方式 静默给错候选,用户以为参数不存在 子进程失败则整段补全消失

动态补全的两笔隐藏账单:每条补全路径都要处理无网络、无凭据,否则离线时退化成全量回退;生成脚本要打版本戳,否则出现"补全里有这个子命令,执行却报 unknown command"——比没有补全更伤信任。

更省的选择是只提供机器可读帮助(--help-json),把补全交给通用框架生成。


3. 帮助文本的四个层次

mycli deploy --app <NAME> [--env ENV] [--dry-run]   # 第 1 层:子命令列表里的一行摘要
Usage:                                              # 第 2 层:用法与参数
  mycli deploy --app NAME [--env ENV] [--tag TAG] [--dry-run]
Examples:                                           # 第 3 层:示例
  mycli deploy --app api --env staging
  mycli deploy --app api --tag v1.2.3 --dry-run
Flags:
  --app    required  应用名,取自 mycli apps
  --env    default: staging   可选 staging|prod
层次 内容 字数预算 缺失后果
一行摘要 这个子命令做什么 ≤ 60 字 只能靠子命令名猜
用法 参数形状、互斥 2–4 行 靠试错
示例 常见组合的完整命令 2–4 条 转去问人
进阶 默认值来源、边界条件 折叠到 --help --all 卡住后翻源码

示例比参数表有效,原因不是"更友好",而是信息形态不同:参数表描述取值域,用户脑子里装的是任务。"把一个 tag 部署到 staging"和 --env 的枚举之间隔着一层翻译,示例把这层翻译预先做掉了。参数表能机械生成,示例不能——所以示例才是帮助里唯一含判断力的部分。

硬规则:--help 必须一屏可见,超出的折叠。长度不是问题,"翻两页才看到示例"才是。


4. 错误提示的三级

# 一级:等于没说
error: invalid argument

# 二级:说清怎么改
error: unknown flag "--dryrun"
  did you mean "--dry-run"?  run: mycli deploy --help

# 三级:给出可直接复制的命令
error: --env "stg" is not valid; valid values: staging, prod
  retry: mycli deploy --app api --env staging
级别 输出 用户下一步 实现成本
一级 参数名 去翻 --help 一行 printf
二级 最近邻建议(编辑距离) 复制建议 一个相似度函数
三级 已回填参数的完整命令 复制即执行 需要上下文里其余参数

第三级值得做的理由很实在:用户已经完整输入过一次,那些值都在内存里,回填是零额外信息量的操作,却是失败瞬间的全部。两条约束:错误提示走 stderr,不污染管道;非交互环境下关掉颜色和边框,否则 CI 日志全是乱码。


5. 参数的可猜性

设计点 可猜的做法 不可猜的做法
命名 --environment + -e --en、--envname
必需项 缺失在解析阶段就报错 运行到一半才报缺
默认值 --help 里标 default: 埋在代码里,环境间行为莫名不同
布尔开关 --dry-run / --no-verify 成对 无法在命令行覆盖配置文件里的开关
取值来源 枚举列出,且可用命令查 要求用户去文档抄名字

6. 双模:人和脚本共用一套接口

面向 特征 设计要求 典型错误
人 有 TTY、会中断、看进度 进度、颜色、确认、分页 脚本模式下也输出进度条
脚本 无 TTY、set -e、解析 stdout 稳定输出、退出码语义、--json 人类提示混进 stdout
# 行为分流:只在 TTY 上交互
[ -t 0 ] || { dry_run=1; assume_yes=0; }

# 输出分流:stdout 只放数据,诊断信息走 stderr
$ mycli status --json | jq -r '.services[].name'
$ mycli status          # 人读的表

退出码是脚本模式下最重要的"错误提示":0 成功、1 业务失败、2 用法错误、>2 留给具体语义。if mycli ...; then 的可靠性全押在这套约定上。


7. 破坏性操作:确认与 --dry-run

$ mycli rollback --app api --to v1.1.0 --dry-run
would rollback: api
  current: v1.2.3
  target : v1.1.0
  effect : 2 pods replaced, no data migration
  • 确认要求输入具体资源名(type "api" to confirm),[y/N] 挡不住误操作;
  • 无 TTY 时默认拒绝破坏性操作,除非显式 --yes;
  • --dry-run 的输出应是"将要执行的动作列表",不是 DRY RUN OK 这种空话——后者无法验证参数解析是否正确。

8. 子命令上限与弃用提示

子命令数 用户行为 需要的组织手段
≤ 7 能记住,直接敲 列表足够
8–15 记大致名字,靠补全 分组 + 别名
16–30 只记常用 5 个 按任务分组,而非字母序
> 30 放弃,退回旧办法 拆工具,或改为配置驱动

弃用提示只有两个位置有效:执行时打到 stderr,以及 --help 里标注。写在 CHANGELOG 等于没写。

$ mycli deploy --app api --env stg
warning: "--env stg" is deprecated and will fail after 2.0
  use: --env staging      # 不给替代写法,用户只会去搜"为什么 warning"

9. 唯一来源:文档与 --help 不漂移

帮助腐坏的方式很一致:参数在代码里,文案在另一处,示例在 README 里,三份各自演化,半年后没一份对。

# 与参数解析共用一份定义;help 与补全脚本都从它生成
name: deploy
args:
  - {name: app, required: true, help: "应用名,取自 mycli apps"}
  - {name: env, default: staging, enum: [staging, prod]}
  - {name: dry-run, type: bool, help: "只打印动作,不调用后端"}
examples: ["mycli deploy --app api --env staging"]

判据:改动一个参数时需要修改的文件数。理想是 1,等于 2 还能靠自律,3 以上漂移只是时间问题。


10. 排错

现象 真实原因 处理
补全没反应 脚本未 source,或函数名与命令名不匹配 complete -p mycli 查注册
有候选却报 unknown command 补全脚本版本落后 生成脚本带版本戳,升级时重生成
用户仍去问人 帮助里只有参数表,没有任务级示例 补 2–4 条完整命令
报错后重复同样的命令 提示没给修正路径 输出合法枚举 + 回填命令
脚本解析不了输出 人类提示混进 stdout,颜色未按 TTY 关闭 stdout 只放数据;[ -t 1 ] 判色
CI 里命令挂住 交互确认在无 TTY 下等待输入 非 TTY 默认非交互

11. 边界:什么情况不必设计可发现性

场景 判断 理由
一次性脚本 不设计 生命周期 < 一周,读者是作者
单团队内部工具、< 5 人用 只做错误提示 口头同步便宜过维护补全
已有成熟替代品 不做 用户的肌肉记忆在别处
只被程序调用 改做库/API 可发现性的受众是人
极低频(一季度一次) 只做示例层 补全成本摊到每次使用太高

一旦工具跨过"作者以外的第三个人"和"半年以上"两条线,投入就开始回本——省下的是每次被问"这个参数怎么填"的打断。


12. 度量

指标 采集方式 读法
--help 调用率 参数解析层埋点 过高 = 语法不可猜;为零 = 用户直接放弃
错误后重试成功率 同 session 内失败→成功 低于一半说明没给出修正路径
帮助后立即执行率 --help 后是否紧跟成功执行 偏低说明示例不可直接复制
退回旧办法的比例 命令调用分布 唯一真正重要的指标

13. 自测

  1. 三个入口里哪个产出比最高?为什么不是补全?
  2. 静态补全的失效方式为什么比动态补全更危险?
  3. 为什么示例不能被机械生成,而参数表可以?
  4. 三级错误提示需要哪些信息才能做到?这些信息从哪来?
  5. 工具同时面向人和脚本,stdout 与 stderr 各承担什么?
  6. 什么信号说明该停止增加子命令,而不是继续加帮助文本?
S01-A09 · 命令行的可发现性:补全、帮助与错误提示
https://www.clxhxhhr.top/posts/6224/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。