终端程序遵循的“规则”
最近我一直在思考,终端里发生的一切其实都是以下几者的某种组合:
- 你的操作系统的职责
- 你的shell的职责
- 你的终端模拟器的职责
- 你碰巧正在运行的程序的职责(比如
top或vim或cat)
前三者(操作系统、shell 和终端模拟器)基本上都是已知的量——如果你在 Linux 上用 GNOME Terminal 跑 bash,你大致可以推断出这些东西是如何相互作用的,而且它们的部分行为已由 POSIX 标准化。
但第四个(“你碰巧正在运行的程序”)给人的感觉是它什么都能干。你怎么可能知道一个程序会怎么表现呢?
这篇文章有点长,所以先给出一个简短的目录:
程序的行为出奇地一致
据我所知,关于终端里的程序应该如何表现,并没有真正的标准——我所知道的最接近的东西有:
- POSIX,它主要规定了你的终端模拟器 / OS / shell 应该如何协同工作。我认为它确实对
cp这类核心工具的工作方式规定了一些内容,但据我所知,对于比如htop应该如何表现,它没有任何说法。 - 这份命令行界面指南
不过即使没有标准,根据我的经验,终端里的程序表现得相当一致。所以我想写下一份“规则”清单,这些规则是我经验中程序大多会遵循的。
这些“规则”是描述性的,而不是规范性的
我在这里的目的并不是要说服终端程序的作者们应该遵循其中任何一条规则。这些规则有很多例外,而这些例外往往有充分的理由。
但对我来说,了解一个随手用到的新终端程序会有什么样的行为是非常有用的。与其想“呃,程序真的什么都可能做”,不如想“好吧,这是我所期望的基本规则,然后我在心里记一小份例外清单就行”。
所以我只是把我在使用终端的这 20 年里观察到的程序行为、我认为它们为什么会这样表现,以及一些该规则被“打破”的例子写下来。
并不总是能明显看出哪些“规则”是程序需要负责实现的
有一堆常见的约定,我认为它们显然是程序需要负责实现的,比如:
- 配置文件应该放在
~/.BLAHrc或~/.config/BLAH/FILE或/etc/BLAH/之类的位置 --help应该打印帮助文本- 程序应该把“常规”输出打印到 stdout,把错误打印到 stderr
但这篇文章要聚焦的是那些并非百分之百明显属于程序职责的事情。比如说,按下 Ctrl-D 就退出 REPL,在我看来就像一条“自然法则”,但程序往往需要显式实现对它的支持——cat 不需要实现 Ctrl-D 支持,而 ipython 却需要。(下文“规则 3”中会详细讲)
理解了哪些事情是程序的职责之后,当不同程序的实现略有不同时,你就不会那么惊讶了。
规则 1:非交互式程序在你按下 Ctrl-C 时应当退出
这条规则的主要原因是:如果非交互式程序没有设置 SIGINT 信号处理器,它们默认就会在收到 Ctrl-C 时退出,所以这条规则有点像“你应该表现得和默认行为一致”。
让很多人困惑的是,这条规则不适用于交互式程序,比如 python3、bc 或 less。这是因为在交互式程序里,Ctrl-C 有不同的职责——如果程序正在执行某个操作(比如 less 里的一次搜索,或 python3 里的一段 Python 代码),那么 Ctrl-C 会中断那个操作,但不会停止程序本身。
作为交互式程序中这一机制的一个例子:这里是 prompt-toolkit 中的代码(prompt-toolkit 是 iPython 用来处理输入的库),当你按下 Ctrl-C 时它会中止一次搜索。
规则 2:TUI 程序在你按下 q 时应当退出
TUI 程序(比如 less 或 htop)通常会在你按下 q 时退出。
这条规则不适用于那些按 q 退出没有意义的程序,比如 tmux 或文本编辑器。
规则 3:REPL 在空行上按下 Ctrl-D 时应当退出
REPL(比如 python3 或 ed)通常会在你在空行上按下 Ctrl-D 时退出。这条规则与 Ctrl-C 那条类似——原因是:默认情况下,如果你以“cooked 模式”运行一个程序(比如 cat),那么当你在空行上按下 Ctrl-D 时,操作系统会返回一个 EOF。
我用的大多数 REPL(sqlite3、python3、fish、bash 等)实际上并不使用 cooked 模式,但它们都实现了这个快捷键,以模仿默认行为。
例如,这里是 prompt-toolkit 中在按下 Ctrl-D 时退出的代码,这里是 readline 中的同样代码。
直到最近我都以为这是一条“终端物理定律”,因为我几乎从没见过它被打破,但从上面的链接可以看到,这只是每个输入库各自需要实现的东西。
有人指出 Erlang 的 REPL 在你按下 Ctrl-D 时并不会退出,所以我想并不是每个 REPL 都遵循这条“规则”。
规则 4:不要使用超过 16 种颜色
终端程序很少使用基础 16 色 ANSI 颜色之外的颜色。这是因为如果你用十六进制代码指定颜色,很可能会与某些用户的背景色冲突。比如,如果我以 #EEEEEE 打印一段文字,它在白色背景上几乎看不见,不过在深色背景上看起来没问题。
但如果你坚持使用默认的 16 种基础色,用户就很可能已经在他们的终端模拟器里配置过这些颜色,使它们与背景色搭配得比较合理。坚持默认基础 16 色的另一个理由是,这样对终端模拟器支持哪些颜色的假设更少。
我通常见到的打破这条“规则”的程序只有文本编辑器,比如 Helix 默认会使用紫色背景,而这并不是一种默认 ANSI 颜色。Helix 打破这条规则似乎没什么问题,因为 Helix 不是“核心”程序,而且我猜任何不喜欢那个配色方案的 Helix 用户都会直接换个主题。
规则 5:大致支持 readline 键位绑定
只要说得通,我用的几乎所有程序都支持 readline 键位绑定。比如,下面是一堆不同的程序,以及它们定义 Ctrl-E 跳到行尾的位置的链接:
- ipython(此处定义了 Ctrl-E)
- atuin(此处定义了 Ctrl-E)
- fzf(此处定义了 Ctrl-E)
- zsh(此处定义了 Ctrl-E)
- fish(此处定义了 Ctrl-E)
- tmux 的命令提示符(此处定义了 Ctrl-E)
这些程序没有一个真正直接使用 readline,它们只是在某种程度上模仿 emacs/readline 的键位绑定。它们的模仿并不总是完全一致:比如 atuin 似乎把 Ctrl-A 用作前缀,所以 Ctrl-A 不能跳到行首。
另外,所有这些程序似乎都实现了自己的内部剪切和粘贴缓冲区,因此你可以用 Ctrl-U 删除一行,然后用 Ctrl-Y 把它粘贴回来。
这方面的例外有:
- 有些程序(比如
git、cat和nc)完全没有行编辑支持(除了退格键、Ctrl-W和Ctrl-U) - 一如既往,文本编辑器是个例外,每个文本编辑器都有自己编辑文本的方式
关于“一个程序支持哪些键位绑定?”这个问题,我在在终端中输入文字很复杂一文中写了更多内容。
规则 5.1:Ctrl-W 应当删除最后一个词
我从未见过哪个程序(文本编辑器除外)中 Ctrl-W 不删除最后一个词。这与 Ctrl-C 那条规则类似——默认情况下,如果一个程序处于“cooked 模式”,操作系统会在你按下 Ctrl-W 时删除最后一个词,在你按下 Ctrl-U 时删除整行。所以通常程序会模仿这种行为。
除了文本编辑器之外,我想不出任何例外,但如果有的话我很乐意听听!
规则 6:写入管道时禁用颜色
大多数程序在写入管道时会禁用颜色。例如:
rg blah会高亮输出中所有出现的blah,但如果输出到管道或文件,它会关闭高亮。ls --color=auto在写到终端时会使用颜色,但在写到管道时不会。
这两个程序在写到终端时还会以不同的方式格式化输出:ls 会把文件按列组织,ripgrep 会用标题把匹配结果分组。
如果你想强制程序使用颜色(比如因为你想看看颜色效果),可以用 unbuffer 强制程序的输出变成 tty,像这样:
unbuffer rg blah | less -R
我确信有一些程序会“打破”这条规则,但我现在想不出任何例子。有些程序带有 --color 标志,可以用来强制开启颜色;在上面的例子中,你也可以用 rg --color=always | less -R。
规则 7:- 表示 stdin/stdout
通常,如果你给程序传 - 而不是文件名,它会从 stdin 读取或写到 stdout(视哪种合适而定)。比如,如果你想用 black 格式化剪贴板上的 Python 代码然后再复制回来,你可以运行:
pbpaste | black - | pbcopy
(pbpaste 是 Mac 上的程序,在 Linux 上你可以用 xclip 做类似的事情)
我的印象是,只要说得通,大多数程序都会实现这一点,我现在想不出任何例外,但我确信例外还有很多。
这些“规则”需要很长时间才能学会
这些规则花了我很长时间才学会,因为我必须:
- 先了解到这条规则居然普遍适用(“
Ctrl-C能退出程序”) - 注意到一些例外(“好吧,
Ctrl-C能退出find,但不能退出less”) - 下意识地弄清楚规律是什么(“
Ctrl-C通常会让非交互式程序退出,但在交互式程序里,它可能会中断当前操作而不是退出程序”) - 最终也许把它总结成一条我自己明确知道的规则
说实话,我对终端的很多理解仍然停留在“下意识的模式识别”阶段。我之所以愿意花时间把这些东西明确写出来,唯一的原因是我一直在尝试向别人解释它是如何工作的。希望把这些“规则”明确地写下来,能让别人学起来稍微快一点。
随机一篇博客