"Rules" that terminal programs follow

Julia Evans

ターミナルプログラムが従う「ルール」

原文は Julia Evans により に公開されました。 このブログを購読する

最近、ターミナルで起こることはすべて、次のいずれかの組み合わせで成り立っていると考えるようになりました。

  1. OSの仕事
  2. シェルの仕事
  3. ターミナルエミュレータの仕事
  4. たまたま実行しているプログラムtopvimcat など)の仕事

最初の3つ(OS、シェル、ターミナルエミュレータ)については、ある程度わかっているものとして扱えます。たとえば 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 のルールと似ています。理由は、デフォルトでは「cooked mode」でプログラム(cat など)を実行している場合、空行で Ctrl-D を押すと OS が 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 は「コア」なプログラムではないですし、このカラースキームが気に入らないユーザーはテーマを変更するだろうと思うので、このルールを破っても問題ないように思えます。

ルール5: readline のキーバインドをなんとなくサポートする

私が使っているプログラムのほとんどは、意味がある場合には readline のキーバインドをサポートしています。たとえば、行末に移動する Ctrl-E を定義している箇所へのリンクとともに、いくつか例を挙げます。

これらのプログラムは、実際にはどれも直接 readline を使っているわけではなく、emacs/readline のキーバインドをなんとなく模倣しているだけです。しかも完全に同じように模倣しているわけではありません。たとえば atuin は Ctrl-A をプレフィックスとして使っているようで、Ctrl-A で行頭に移動することはありません。

また、これらのプログラムはすべて独自の内部的なカット&ペースト用バッファを実装しているようで、Ctrl-U で行を削除し、Ctrl-Y で貼り付けることができます。

この例外は次のとおりです。

  • gitcatnc のような一部のプログラムは、行編集機能をまったく持っていません(backspace、Ctrl-WCtrl-U を除く)
  • 例によってテキストエディタは例外で、どのエディタもテキスト編集には独自のアプローチを持っています

この「プログラムはどんなキーバインドをサポートしているのか?」という問題については、ターミナルでのテキスト入力は複雑で詳しく書きました。

ルール5.1: Ctrl-W で直前の単語を削除すべき

(テキストエディタ以外で)Ctrl-W で直前の単語が削除されないプログラムを、私は見たことがありません。これは Ctrl-C のルールと似ています。デフォルトでは、プログラムが「cooked mode」にある場合、Ctrl-W を押すと OS が直前の単語を削除し、Ctrl-U を押すと行全体を削除します。そのため、通常プログラムはこの挙動を模倣します。

テキストエディタ以外でこの例外を思いつきませんが、もしあればぜひ教えてほしいです!

ルール6: パイプに書き出すときは色を無効にする

ほとんどのプログラムは、パイプに書き出すときに色を無効にします。たとえば次のような例があります。

  • rg blahblah の出現箇所をすべてハイライトしますが、出力先がパイプやファイルの場合はハイライトをオフにします。
  • 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. 最終的に、それを自分が理解できる明示的なルールとしてまとめる

正直なところ、ターミナルについての私の理解の多くは、まだ「無意識のパターン認識」の段階にあります。わざわざ時間をかけて明示的にしようとしてきた唯一の理由は、それがどう動くのかを他の人に説明しようとしてきたからです。これらの「ルール」を明示的に書き留めることで、他の人がこのあたりを学ぶのが少しでも早くなれば嬉しいです。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント