ターミナルプログラムが従う「ルール」
最近、ターミナルで起きることはすべて、次のいずれかの組み合わせなのだと考えていました。
- 使っているオペレーティングシステムの仕事
- 使っているシェルの仕事
- 使っているターミナルエミュレーターの仕事
- そのとき実行しているプログラムの仕事(
topやvim、catなど)
最初の3つ(オペレーティングシステム、シェル、ターミナルエミュレーター)は、どれもある程度は既知のものです。Linux上のGNOME Terminalでbashを使っているなら、それらがどう連携するかはだいたい推測できますし、動作の一部はPOSIXによって標準化されています。
でも4つ目の「そのとき実行しているプログラム」は、何でもできてしまいそうに感じます。プログラムがどう動くかなんて、どうやって知ればいいのでしょうか。
この記事は少し長いので、簡単な目次を用意しました。
プログラムは驚くほど一貫した動作をする
私の知る限り、ターミナル上のプログラムがどう動くべきかを定めた本当の意味での標準はありません。近いものとして知っているのは、次の2つくらいです。
- POSIX。これは主に、ターミナルエミュレーター、OS、シェルがどう連携すべきかを定めています。
cpのようなコアユーティリティの動作についてもいくつか規定していると思いますが、私の知る限り、たとえばhtopの動作については何も規定していません。 - このコマンドラインインターフェースのガイドライン
それでも、標準がないにもかかわらず、私の経験ではターミナル上のプログラムはかなり一貫して動作します。そこで、私の経験上、プログラムがだいたい従っている「ルール」を書き出してみることにしました。
これは規範ではなく、記述のためのものです
ここでの目的は、ターミナルプログラムの作者に、これらのルールに従うべきだと説得することではありません。これらには例外がたくさんありますし、その例外にはたいてい、もっともな理由があります。
ただ、初めて使うターミナルプログラムがどう動くのかを予想できるのは、とても便利です。「プログラムは文字どおり何でもできる」と考える代わりに、「基本的にはこういうルールだと考えておいて、例外だけ少し覚えておけばいい」と考えられるからです。
ここでは、ターミナルを20年間使ってきた中で、プログラムがどう動くのを見てきたか、なぜそう動くのだと思うのか、そしてそのルールが「破られる」例をいくつか書き留めます。
どの「ルール」をプログラムが実装すべきなのかは、いつも明らかとは限りません
プログラムが実装すべきものだと、かなりはっきり分かる一般的な慣習はいくつもあります。たとえば次のようなものです。
- 設定ファイルは
~/.BLAHrcや~/.config/BLAH/FILE、/etc/BLAH/などに置く --helpを指定するとヘルプテキストを表示する- プログラムは「通常の」出力をstdoutに、エラーをstderrに出力する
ただ、この記事で扱うのは、プログラムが実装すべきものだと100%明らかなわけではない事柄です。たとえば、REPLでCtrl-Dを押すと終了するというのは、私には「自然の法則」のように感じられます。しかし実際には、プログラム側で明示的に対応を実装しなければならないことがよくあります。catにはCtrl-Dへの対応を実装する必要がないのに、ipythonには実装されています(この点については、後述する「ルール3」で詳しく説明します)。
どの動作をプログラムが担当すべきなのかを理解しておけば、プログラムによって実装が少しずつ違っていても、そこまで驚かずに済みます。
ルール1:非対話型プログラムはCtrl-Cを押すと終了する
このルールの主な理由は、非対話型プログラムがSIGINTのシグナルハンドラーを設定していなければ、デフォルトではCtrl-Cで終了するからです。つまり、これは「デフォルトと同じように動作させるべき」というルールです。
多くの人が混乱するのは、これがpython3やbc、lessのような対話型プログラムには当てはまらない点です。対話型プログラムでは、Ctrl-Cには別の役割があるからです。プログラムが何らかの処理(たとえばlessでの検索や、python3で実行中のPythonコード)を行っている場合、Ctrl-Cはその処理を中断しますが、プログラム自体は停止しません。
対話型プログラムでの動作例として、Ctrl-Cを押したときに検索を中断する、prompt-toolkit(iPythonが入力処理に使っているライブラリ)のコードを見てみましょう。
ルール2:TUIはqを押すと終了する
TUIプログラム(lessやhtopなど)は、通常、qを押すと終了します。
qを押して終了するのが意味をなさないプログラムには、このルールは当てはまりません。tmuxやテキストエディターなどがその例です。
ルール3:REPLは空行でCtrl-Dを押すと終了する
REPL(python3やedなど)は、通常、空行でCtrl-Dを押すと終了します。このルールはCtrl-Cのルールと似ています。プログラム(catなど)を「カノニカルモード」で実行している場合、デフォルトでは、空行でCtrl-Dを押すとOSがEOFを返すからです。
私が使うREPLの多く(sqlite3、python3、fish、bashなど)は、実際にはカノニカルモードを使っていません。それでも、デフォルトの動作を再現するため、このキーボードショートカットを実装しています。
たとえば、Ctrl-Dを押すと終了するprompt-toolkitのコードがあります。また、readlineにも同じコードがあります。
私はごく最近まで、これを「ターミナル物理学の法則」だと思っていました。実際、これが破られるのをほとんど見たことがなかったからです。しかし、上のリンクから分かるように、これは各入力ライブラリがそれぞれ実装しなければならない機能にすぎません。
誰かが、ErlangのREPLはCtrl-Dを押しても終了しないと教えてくれました。どうやら、すべてのREPLがこの「ルール」に従うわけではないようです。
ルール4:16色を超える色を使わない
ターミナルプログラムが基本の16色のANSIカラー以外の色を使うことは、めったにありません。色を16進数で指定すると、ユーザーの背景色とぶつかる可能性が非常に高いからです。たとえば、#EEEEEEとしてテキストを出力すると、白い背景ではほとんど見えませんが、暗い背景では問題なく見えます。
一方、デフォルトの16色に限定すれば、ユーザーがターミナルエミュレーターで、背景に対して適切に見えるよう色を設定している可能性がずっと高くなります。デフォルトの基本16色を使うもう1つの理由は、ターミナルエミュレーターがどの色をサポートしているかについて、前提を置かずに済むことです。
私がこの「ルール」に反しているのをよく見るのは、テキストエディターだけです。たとえばHelixは、デフォルトで、ANSIのデフォルトカラーにはない紫色の背景を使います。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を押すとOSが直前の単語を削除し、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に書き込みます。たとえば、クリップボードにあるPythonコードをblackでフォーマットしてからコピーしたい場合は、次のように実行できます。
pbpaste | black - | pbcopy
(pbpasteはMacのプログラムです。Linuxではxclipを使って同じようなことができます。)
私の印象では、意味がある場合にはほとんどのプログラムがこの動作を実装しています。今のところ例外は思いつきませんが、実際にはたくさんあるはずです。
こうした「ルール」を身につけるには長い時間がかかる
私がこれらのルールを身につけるのに長い時間がかかったのは、次の段階を踏む必要があったからです。
- そのルールがそもそも適用されることを知る(「
Ctrl-Cでプログラムが終了する」) - いくつかの例外に気づく(「なるほど、
Ctrl-Cでfindは終了するけれど、lessは終了しないのか」) - パターンが何なのかを無意識に理解する(「
Ctrl-Cは一般に非対話型プログラムを終了させるが、対話型プログラムでは終了させずに現在の処理を中断することがある」) - 最終的には、それを自分が知っている明示的なルールとしてまとめる
私のターミナルに関する理解の多くは、正直なところ、今でも「無意識のパターン認識」の段階にあります。わざわざ物事を明示的にしている唯一の理由は、ターミナルの仕組みを他の人に説明しようとしてきたからです。こうした「ルール」を明示的に書き出すことで、他の人がこのあたりのことを少し早く学べるようになればと思います。
記事をランダムに読む