命令行里的设计艺术

1,267 阅读4分钟
原文链接: blog.joway.io

在谈论手机 App 或是网页时,我们总会谈到交互设计,但倘若涉及到面向专业用户的 CLI(Command-line interface)领域,很少有人会将它与用户交互相联系。甚至在很多人的大脑里,已经把 CLI 和「难用」画上了等号,更有甚者认为 CLI 的难用才体现了其 「专业性」。

与此同时,「RTFM」(Read The Fucking Manual) 作为一个梗在工程师群体广泛流传,也让许多作者对于其不好用的 CLI 有了一个天然的借口。

虽然我们都知道阅读文档是一个好习惯,但恐怕大部分人在用一个新命令前都不会去仔细阅读完它的文档手册,就算你今天读了,你不可能永远记得,也不可能每次记忆模凌两可的时候都去重读一遍。所以可想而知一个高度依赖文档用户才能够正常使用的 CLI 不是一个合格的 CLI。

以下是我对于命令行设计的一些个人观点与观察,所涉并不一定广泛和正确,仅作为抛砖引玉,也欢迎在评论里提出你的看法。

遵循约定俗成

例如 mv, cp, ln 这些命令都遵循 [action] [source_file] [target_file] 的格式,这不一定很有逻辑,但既然都已经约定俗成了,如果你违犯这个顺序要倒过来,其实是一件蛮缺德的事情。为什么说「缺德」?因为一个用过了你的命令的人,他很容易开始怀疑是不是还有别的命令行打破了这个约定,他会对其它命令也不放心,最后他甚至会忘了真正被广泛采用的约定用法是什么了,导致每次用类似语法的命令都胆颤心惊,这点我深有体会 😔。

所以如果你的命令有类似的约定俗成可以遵守,你应该遵守业内的这种约定,这是一种职业道德。

一致的命令组织结构

对所有和用户打交道的产品来说,「一致性」是天条。我遇到过两种被广泛采用的组织风格:

$ [cmd] [module] [flags] [args]

$ [cmd] [action] [flags] [args]

无论是按模块划分还是按行为划分本质思路其实都是一样的,有些人会认为项目大了会难以遵守这套规范,但即便是目前规模已经非常庞大的 kubectl,它依旧坚持以 [cmd] [action] [flags] [args] 为基础的设计准则。在有些较为复杂的地方,它可以用 subcmd 来进一步向外部隐藏复杂性 [cmd] [subcmd] [action] [flags] [args],例如:

$ kubectl config [action] [flags] [args]

我几乎每天都会用到 kubectl,但我的确很少去看它的文档,甚至我都想不到我是怎么就会使用它的。越是在复杂的项目面前,这种一致性带来的好处越明显。

而一个典型的反面教材是 Git。它是我用过的最复杂同时又是最混乱的 CLI,以一般人最常用的分支操作举例:

# create branch
$ git checkout -b new_branch

# delete branch
$ git branch -D new_branch

你会发现对于创建和删除分支这种最基本的命令,它 checkout 是一个动词,branch 又是一个名词,前者 -b 来代表 branch 这个名词,后者 -D 来代表 delete 这个动词。短短两行命令,还是最常用的两行就让我们看到这种设计是多么没有逻辑。更别说还有 git pullgit fetch 这种了。

面向用户而非面向实现

对于 Git 的设计,它的拥护者会声称如果从 Git 底层实现的视角来看这种设计才能够明白它的用意,但这种辩解隐藏的一个意思是,使用者不仅要读使用文档,还要去钻研 Git 的底层实现,这样才能够顺利使用这个工具。

对于 CLI 工具,尤其是 Git 这类注定会被大规模用户长时间使用的工具,指望所有人都能够弄懂它的底层实现是不现实的。出于对用户负责的角度,更应当将底层抽象成一个更容易被人理解的模型,面向这个模型去封装底层实现。而不是反过来借用你的命令行设计让用户去理解底层实现。Git 或许还有这个资格要求用户去学习,其它命令行可能就没这种底气了。

繁琐胜过歧义

在歧义这点上最让我痛苦的是 mysql 的命令:

$ mysql -u username -p password -P prot

$ ssh -p port
$ redis-cli -p port -a password

可以看到对于其它很多 CLI,-p 指的都是 port,但是在 mysql 里 -P 才是 port,虽然 mysql 有它自己的理由,但这种设计实实在在给用户造成了很大的困扰,像 redis 就选择用 -a 来取代 -p 作为 password,给 port 腾出位置。

甚至于我比较激进地认为,不应当推荐使用大写的缩写方式,我们记住 -p 是依赖于 port 的首字母,但是对于大小写我们没有可以辅助联想记忆的地方,非常容易搞混。

所以相比与为了偷懒搞那么多缩写,我更愿意使用能够联想记忆的全拼参数。比如为了防止出现上面 port 和 password 两个参数不一致的情况,我现在经常是手动打全 --port 的,对我来说,按回车的心情舒畅比提心吊胆地用缩写要来得划算许多。

安全第一

如果你的命令会造成一些不可逆的行为,在设计时应该首先把安全性放在第一位。假设一个命令 wm 是把一张图片作为水印打在另一张图片上,并且会覆盖原图,那么我就不会设计成这种形式:

$ wm [watermark_file] [source_file]

在上面的形式里,你想象的语义是 watermark [watermark_file] on [source_file],但用户想的可能是 watermark [source_file] with [watermark_file]。你不能确保这个,即便是所谓的约定俗成用法也没法给予你这个保障。所以我会偏向于采用这样参数的设计:

$ wm --texture [watermark_file] [source_file]

这样用户看到这个命令就应该明白是怎么个处理,按下回车的时候心情能够愉悦许多。