终端程序遵循的“规则”
原文由 Julia Evans 于 发布,订阅该博客
最近我一直在思考,终端里发生的一切,其实都是以下几者组合的结果:
- 你的操作系统负责的部分
- 你的shell负责的部分
- 你的终端模拟器负责的部分
- 你当前正在运行的那个程序负责的部分(比如
top、vim或cat)
前三者(操作系统、shell 和终端模拟器)都算是比较确定的因素——如果你在 Linux 上用 GNOME Terminal 跑 bash,大体上能推断出它们是如何协同工作的,而且它们的一部分行为是由 POSIX 标准化定义的。
但第四类(也就是“你恰好正在运行的那个程序”)感觉就可能做出任何事情。你怎么知道一个程序会如何表现呢?
这篇文章有点长,所以先放一个简要目录:
程序的表现其实惊人地一致
据我所知,终端程序该如何表现并没有什么真正的标准——我所知道的最接近标准的是:
- POSIX,它主要规定了你的终端模拟器 / 操作系统 / 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(iPython 用来处理输入的库)中在你按下 Ctrl-C 时中止搜索的代码。
规则 2:TUI 程序在你按下 q 时应该退出
TUI 程序(比如 less 或 htop)通常会在你按下 q 时退出。
这条规则不适用于那些按 q 退出本来就不合理的程序,比如 tmux 或文本编辑器。
规则 3:REPL 在空行按下 Ctrl-D 时应该退出
REPL(比如 python3 或 ed)通常会在空行上按下 Ctrl-D 时退出。这条规则和 Ctrl-C 的规则类似——原因在于,默认情况下如果你在“规范模式(cooked mode)”下运行一个程序(比如 cat),那么当你在空行按下 Ctrl-D 时,操作系统会返回一个 EOF。
我常用的大多数 REPL(sqlite3、python3、fish、bash 等)实际上并没有使用规范模式,但它们都还是实现了这个快捷键,来模仿默认行为。
比如,这里是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 的规则类似——默认情况下,如果程序处于“规范模式”,操作系统在你按下 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通常会退出非交互式程序,但在交互式程序里,它可能会中断当前操作而不是退出整个程序”) - 最终也许能把它归纳成一条明确的规则
老实说,我对终端的很多理解仍然停留在“潜意识的模式识别”阶段。我之所以会花时间把这些东西明确地写出来,完全是因为我想试着给别人解释它的工作原理。希望把这些“规则”明确地写下来,能让其他人学习这些东西的速度快一些。
随机一篇博客
评论
登录后参与讨论