最近、ターミナルで起こるすべてのことは、次のいずれかの組み合わせであると考えています:

  1. あなたのオペレーティングシステムの仕事
  2. あなたのシェルの仕事
  3. あなたの端末エミュレータの仕事
  4. 実行しているプログラムの仕事(topvimcat など)

最初の3つ(オペレーティングシステム、シェル、端末エミュレータ)は、ある程度既知の要素です。Linux上のGNOME Terminalでbashを使用している場合、これらの要素がどのように相互作用するかをある程度推論でき、その動作の一部はPOSIXによって標準化されています。

しかし4つ目(「実行しているプログラム」)は、何でもできてしまうように感じます。プログラムがどのように動作するかをどうやって知ればよいのでしょうか?

この投稿は少し長いので、目次を用意しました:

プログラムは意外と一貫して動作する

私の知る限り、ターミナル内のプログラムがどのように動作すべきかについての本当の標準はありません。最も近いものは以下のものです:

  • POSIX:主に端末エミュレータ/OS/シェルがどのように連携して動作するかを規定しています。コアユーティリティ(cp など)の動作については一部指定していると思いますが、例えば htop がどのように動作すべきかについては何も述べていないと思います。
  • コマンドラインインターフェースガイドライン

しかし、標準がないにもかかわらず、私の経験ではターミナル内のプログラムはかなり一貫した方法で動作します。そこで、プログラムが主に守っている「ルール」のリストを書き留めたいと思いました。

これは記述的であって、規範的ではない

ここでの私の目的は、ターミナルプログラムの作者に対してこれらのルールのいずれかに従うべきと説得することではありません。これらのルールには多くの例外があり、多くの場合その例外には正当な理由があります。

しかし、ランダムに新しいターミナルプログラムを使用する際に、どのような動作を期待すればよいかを知ることは非常に有用です。「うーん、プログラムは文字通り何でもできる」と考えるのではなく、「基本的なルールはこれだ。例外は短いリストとして覚えておけばよい」という考え方ができます。

そこで、私は20年間ターミナルを使って観察してきたプログラムの動作パターン、それらがそのように動作する理由、そしてそのルールが「破られる」ケースの例を書き留めているだけです。

どの「ルール」がプログラムの実装責任なのか、常に明らかとは限らない

プログラムの実装責任であることがかなり明確な一般的な慣習がいくつかあります。例えば:

  • 設定ファイルは ~/.BLAHrc~/.config/BLAH/FILE/etc/BLAH/ などに置くべき
  • --help でヘルプテキストを表示する
  • プログラムは通常の出力を stdout に、エラーを stderr に出力する

しかしこの投稿では、プログラムの責任であることが100%明らかではない事柄に焦点を当てます。例えば Ctrl-D を押すと REPL が終了するのは「自然の法則」のように感じられますが、プログラムはしばしば明示的にサポートを実装する必要があります。catCtrl-D のサポートを実装する必要はありませんが、ipython実装しています。(詳細は後述の「ルール3」を参照)

どの事柄がプログラムの責任であるかを理解すると、異なるプログラムの実装がわずかに異なる場合でも、それほど驚かなくなります。

ルール1: 非対話型プログラムは Ctrl-C で終了すべき

このルールの主な理由は、非対話型プログラムは SIGINT シグナルハンドラを設定しなければデフォルトで Ctrl-C で終了するため、これは「デフォルトのように振る舞うべき」というルールだからです。

多くの人を混乱させるのは、これが python3bcless などの対話型プログラムには適用されないことです。対話型プログラムでは Ctrl-C の役割が異なるためです。プログラムが操作(less での検索や python3 での Python コードの実行など)を実行している場合、Ctrl-C はその操作を中断しますが、プログラム自体は停止しません。

対話型プログラムでの動作例として、Ctrl-C を押したときに検索を中止する prompt-toolkit のコード(iPython が入力処理に使用するライブラリ)を示します。

ルール2: TUIは q で終了すべき

TUIプログラム(lesshtop など)は通常 q を押すと終了します。

このルールは、q を押して終了することが意味をなさないプログラム(tmux やテキストエディタなど)には適用されません。

ルール3: REPLは空行で Ctrl-D を押すと終了すべき

REPL(python3ed など)は通常、空の行で Ctrl-D を押すと終了します。このルールは Ctrl-C のルールと似ています。理由は、プログラム(cat など)を「cooked mode」で実行している場合、デフォルトでオペレーティングシステムが空の行で Ctrl-D を押すと EOF を返すためです。

私が使用するREPLのほとんど(sqlite3、python3、fish、bash など)は実際には cooked mode を使用していませんが、すべてデフォルトの動作を模倣するためにこのキーボードショートカットを実装しています。

例として、Ctrl-D を押すと終了する prompt-toolkit のコードと、readline の同じコードを示します。

私は最近までこれを「ターミナルの物理法則」の一つだと思っていました。なぜなら、これが破られるのを見たことがほとんどなかったからです。しかし、上記のリンクを見ると、これは各入力ライブラリが個別に実装しなければならないものであることがわかります。

Erlang REPL は Ctrl-D を押しても終了しないという指摘があり、すべての REPL がこの「ルール」に従うわけではないようです。

ルール4: 16色を超えて使用しない

ターミナルプログラムが基本の16 ANSI色以外の色を使用することは稀です。これは、16進コードで色を指定すると、ユーザーの背景色と衝突する可能性が非常に高いためです。例えば、テキストを #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」にある場合、OS は 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 へ書き込みます。例えば、クリップボードにある Python コードを black でフォーマットしてからコピーしたい場合は、次のように実行できます:

pbpaste | black - | pbcopy

pbpaste は Mac のプログラムで、Linux では xclip で同様のことができます)

私の印象では、ほとんどのプログラムは意味がある場合にこれを実装しており、今のところ例外を思いつきませんが、例外は確かにあると思います。

これらの「ルール」を学ぶには時間がかかる

これらのルールを学ぶのに長い時間がかかりました。なぜなら、私は以下を行う必要があったからです:

  1. そのルールがどこにでも適用されることを学ぶ(「Ctrl-C でプログラムを終了する」)
  2. いくつかの例外に気づく(「Ctrl-Cfind は終了するが less は終了しない」)
  3. パターンが何であるかを無意識に把握する(「Ctrl-C は一般的に非対話型プログラムを終了するが、対話型プログラムではプログラムを終了する代わりに現在の操作を中断する可能性がある」)
  4. 最終的に、自分が知っている明示的なルールとして定式化する

ターミナルに関する私の理解の多くは、正直なところまだ「無意識のパターン認識」の段階にあります。これらを明示的にする時間を取っている唯一の理由は、他の人にその仕組みを説明しようとしているからです。これらの「ルール」を明示的に書き留めることで、他の人がこのようなことを少しでも早く学べることを願っています。