命令行参数怎么设计?key:value 解析踩过的几个坑
写 shane-new-post 和 shane-new-doc 的过程中,花时间最多的部分其实不是核心的文件生成逻辑,而是命令行参数怎么设计。这篇不讲这两个工具怎么用(前面几篇已经写过),单独聊聊参数格式这块当初是怎么想的,中间改过几次主意。
第一版:照抄常见的 —flag 写法
最开始的直觉是照抄大部分 CLI 工具的习惯,用 --title --tag 这种双横线参数:
new-post --title "Getting Started" --tags "astro,web" --draft false
这种写法的好处是符合大部分人的肌肉记忆,几乎不用学。但写了几个命令之后发现两个不太舒服的地方:
- 字段一多,命令行会变得很长很难读,尤其是标签这种数组类型的字段,用逗号分隔字符串还得额外解析一次
- 每加一个新字段,就要在解析逻辑里多写一段
case "--xxx":的分支,虽然不难,但重复劳动感很强
第二版:换成 key:value 的形式
后来想到,其实不一定非要照抄 flag 风格,换成更紧凑的 key:value 写法反而更适合这个场景:
new-post "getting-started" title:Getting Started/description:An intro post/tags:[astro,web]/draft:F/featured:T/mdx:T
这样设计有几个动机:
- 只有最前面必须的定位参数保留位置固定,比如
new-post只固定文件名一个位置参数,new-doc固定目录和文件名两个位置参数——这两个是”不给就没法继续”的硬性要求,理应排在最前面、位置写死。除此之外的字段全部走key:value,谁在前谁在后完全不重要,写的时候不用死记参数顺序。 - 用
/分隔而不是空格,是因为命令行参数本身经常需要传带空格的值(比如标题”Getting Started”),如果整条命令还要靠空格分隔字段,很容易搞不清哪个空格是字段之间的分隔符、哪个是某个值内部的空格。用/分隔字段、空格留给字段内部的值,读起来反而更清楚。 - 支持用反斜杠换行,方便字段特别多的时候把命令拆成多行、每行一个字段,可读性好很多:
new-post "getting-started" \
mdx:T \
featured:T \
title:Getting Started \
tags:[astro,web] \
draft:F \
description:An intro post
回头看,这算是这两个工具里我自己最满意的一个设计决定——虽然刚接触的人第一次看到 key:value/key:value 这种写法可能会愣一下,但用过一两次基本就记住了,之后打起来比一堆 --flag 明显更快。
布尔值解析:只认”真”,其余一律当”假”
数组和字符串的解析没什么特别的,但布尔值字段(draft、featured、mdx)这块我特意花了点时间想清楚规则,最后定下来的逻辑是:
只有
t或者true(不区分大小写,T、True、TRUE都算)会被识别成”是”,除此之外的任何值——f、false、空字符串、随便打的错别字,甚至压根没写这个键——一律当”否”处理。
写代码的时候这其实对应一个很朴素的判断:
const isTrue = value => /^t(rue)?$/i.test(value ?? "");
不需要单独写一段”判断是不是 false”的逻辑,因为默认值本来就是 false,只要不满足”是真”的条件,自然落回默认值。这个设计的好处是让”异常输入”这件事变得完全可预测——不会因为用户手滑打了个 Ture(拼错的 true)就意外触发某个开关,顶多是没生效而已,不会有更糟的副作用。
键名允许别名,但别名要克制
有些字段我给了不止一个可用的键名,比如 description 可以简写成 desc,directory 可以简写成 dir。加别名的初衷是打字图省事,但别名这东西一旦开了口子就容易收不住——如果每个字段都配三四个别名,参数表反而会变得又长又难记,等于没简化。
所以最后的原则是:只给真的会频繁手打、且缩写足够直观的字段加别名,像 description → desc 这种大家已经有共识的缩写才给,像 title、tags 这种本身已经够短的字段就没必要再加别名了。
不认识的键,warn 而不是 error
另一个刻意做的决定:如果命令里出现了工具不认识的键(比如拼错了字段名,或者从另一个工具的参数表里复制过来忘了改),只打印一条警告,然后跳过这个键继续往下走,不会让整个命令失败。
这么做的理由很实际:新建文章/文档这个动作,失败的代价是”要重新打一遍命令”,而多数情况下用户真正想要的是”能不能把文件先建出来,字段的事等下再改”。如果因为一个无关紧要的可选字段打错字就让整条命令直接报错退出,体验上属于”为了严谨牺牲了容错”,不太符合这类工具本该有的定位——它的核心目标是省事,不是校验。
当然,必填字段是另一回事:标题缺失、目录缺失这种情况必须报错并中止,因为继续执行下去只会生成一个不完整、甚至过不了 schema 校验的文件,报错比生成一个”看起来成功但实际有问题”的文件更负责任。
小结
回头梳理下来,命令行参数设计这件事本质上是在”好记”和”好写”之间找平衡,没有唯一正确答案。对我自己而言,这次的取舍是:位置参数只留最必要的那一两个,其余全部交给顺序自由的 key:value;布尔值只认一种”是”的写法,其余全部安全地落回默认值;容错优先于严格校验,但对真正的必填项绝不放水。写完这两个工具之后,再看很多成熟 CLI 工具的参数设计,会更容易看懂它们当初为什么这么选——很多”看起来只是习惯”的设计,背后往往都能找到具体要解决的问题。