"Rules" that terminal programs follow

Julia Evans

終端機程式遵循的「規則」

原文由 Julia Evans 發布,訂閱此部落格

最近我一直在思考,終端機裡發生的每一件事,其實都是以下幾種職責組合而成的:

  1. 你的作業系統的職責
  2. 你的shell的職責
  3. 你的終端機模擬器的職責
  4. 你當下正在執行的任何程式的職責(像是 topvimcat

前三者(作業系統、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 就會結束,所以這算是一種「就照預設行為做」的規則。

讓很多人困惑的是,這不適用於像 python3bcless 這類互動式程式。這是因為在互動式程式中,Ctrl-C 有不同的任務——如果程式正在執行某個操作(例如在 less 中搜尋,或在 python3 中執行 Python 程式碼),那麼 Ctrl-C 會中斷該操作,但不會結束程式本身。

舉個互動式程式如何運作的例子:這是在 prompt-toolkit(iPython 用來處理輸入的函式庫)中,當你按下 Ctrl-C 時中止搜尋的程式碼。

規則 2:TUI 程式在你按下 q 時應該結束

TUI 程式(像是 lesshtop)通常在你按下 q 時就會結束。

這條規則不適用於任何按 q 離開就不合理的程式,像是 tmux 或文字編輯器。

規則 3:REPL 在空行按下 Ctrl-D 時應該結束

REPL(像是 python3ed)通常在空行按下 Ctrl-D 時就會結束。這條規則跟 Ctrl-C 的規則類似——原因是預設情況下,如果你在「cooked mode」下執行程式(像是 cat),那麼在空行按下 Ctrl-D 時,作業系統會回傳一個 EOF

我常用的大多數 REPL(sqlite3、python3、fish、bash 等)實際上並沒有使用 cooked mode,但它們還是都實作了這個快捷鍵來模擬預設行為。

舉例來說,這是在 prompt-toolkit 中按下 Ctrl-D 就結束的程式碼,而這是在 readline 中同樣的程式碼

老實說,直到最近我都以為這條是「終端機物理定律」,因為我幾乎沒看過它被違反,但從上面的連結可以看出,這只是每個輸入函式庫各自需要去實作的功能。

有人指出 Erlang 的 REPL 按下 Ctrl-D 並不會結束,所以我想並非所有 REPL 都遵守這條「規則」。

規則 4:不要使用超過 16 種顏色

終端機程式很少會使用超過基本的 16 種 ANSI 顏色以外的顏色。這是因為如果你用 hex 色碼指定顏色,很可能會跟某些使用者的背景顏色衝突。舉例來說,如果我把文字印成 #EEEEEE,在白色背景上幾乎會看不見,雖然在深色背景上看起來就沒問題。

但如果你堅持使用預設的 16 種基本顏色,使用者在其終端機模擬器中設定這些顏色的組合,能與其背景顏色搭配得比較合理的機率就高得多。另一個堅持使用預設 16 色的原因是,這樣對終端機模擬器支援哪些顏色的假設會比較少。

我通常看到會打破這條「規則」的只有文字編輯器,例如 Helix 預設會使用紫色背景,這並不是預設的 ANSI 顏色。Helix 打破這條規則似乎也沒什麼關係,因為 Helix 並不是「核心」程式,而且我想任何不喜歡那個配色主題的 Helix 使用者都會自行更換佈景主題。

規則 5:大致上支援 readline 快捷鍵

幾乎我用的每個程式,只要合理的地方都會支援 readline 快捷鍵。舉例來說,這裡有一堆不同程式,以及它們定義 Ctrl-E 跳到行尾的連結:

這些程式其實都沒有直接使用 readline,只是大致上模仿了 emacs/readline 的快捷鍵。而且它們也不總是完全照搬:例如 atuin 似乎把 Ctrl-A 當作前綴鍵,所以 Ctrl-A 並不會跳到行首。

而且所有這些程式似乎都自己實作了內部的剪貼簿緩衝區,所以你可以用 Ctrl-U 刪除一行,然後用 Ctrl-Y 貼上。

例外有:

  • 有些程式(像是 gitcatnc)完全沒有任何行編輯支援(除了退格鍵、Ctrl-WCtrl-U 以外)
  • 一如往常,文字編輯器是例外,每個文字編輯器都有自己編輯文字的方式

關於「程式支援哪些快捷鍵?」這個問題,我在在終端機輸入文字很複雜一文中寫了更多。

規則 5.1:Ctrl-W 應該刪除前一個單字

我從沒看過有哪個程式(文字編輯器除外)在按下 Ctrl-W不會刪除前一個單字的。這跟 Ctrl-C 的規則類似——預設情況下,如果程式處於「cooked mode」,作業系統會在你按下 Ctrl-W 時刪除前一個單字,按下 Ctrl-U 時刪除整行。所以通常程式會模仿這個行為。

除了文字編輯器之外,我想不到任何例外,但如果有的話我很想知道!

規則 6:輸出到 pipe 時應停用顏色

大多數程式在輸出到 pipe 時會停用顏色。例如:

  • rg blah 會在輸出中高亮所有 blah,但如果輸出是導向 pipe 或檔案,就會關掉高亮。
  • ls --color=auto 在輸出到終端機時會使用顏色,但在輸出到 pipe 時就不會

這兩個程式在輸出到終端機時,也會用不同的方式格式化輸出: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 做類似的事)

我的印象是,大多數程式只要合理的話都會實作這個功能,我現在想不到任何例外,但我確定有很多例外。

這些「規則」要花很久才能學會

這些規則花了我很久才學會,因為我必須:

  1. 學到這條規則在任何地方都適用(「Ctrl-C 會結束程式」)
  2. 注意到一些例外(「好吧,Ctrl-C 會結束 find 但不會結束 less」)
  3. 潛意識地搞清楚模式是什麼(「Ctrl-C 通常會結束非互動式程式,但在互動式程式中,它可能會中斷當前操作而不是結束程式」)
  4. 最後也許才能把它整理成一條我明確知道的規則

老實說,我對終端機的很多理解都還停留在「潛意識的模式辨識」階段。我之所以會花時間把這些東西明確化,純粹是因為我想試著向別人解釋它是怎麼運作的。希望把這些「規則」明確寫下來,能讓其他人學習這些東西的速度快一點。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言