- ターミナルで体感する動作は、オペレーティングシステム・シェル・ターミナルエミュレータだけでなく、個々のプログラムの慣習まで合わさって成り立っており、この慣習は思ったより一貫している
- POSIXは主にターミナルエミュレータ・オペレーティングシステム・シェルの相互作用と一部の中核ユーティリティを扱うが、
htopのようなプログラムの細かな挙動までは定めていない
Ctrl-C、q、Ctrl-D、16色のANSIカラー、readlineのキーバインド、パイプ出力時の色の無効化、-のstdin/stdoutとしての意味といったパターンが繰り返し現れる
- 自然法則のように感じられる動作も、実際にはプログラムや入力ライブラリが実装しなければならず、
ipythonのCtrl-D終了やprompt-toolkitのCtrl-Cによる検索中断がその例
- 新しいターミナルプログラムを作ったり、ツールの動作を理解したりするときは、「何でもあり」と考えるよりも、基本的な期待値と例外を先に見るほうが実用的
ターミナル動作を形作る4つの主体
- ターミナルで起きることは、おおむね次の4つの主体が一緒に作る結果と考えられる
- オペレーティングシステム
- シェル
- ターミナルエミュレータ
top、vim、catのような、現在実行中のプログラム
- bash、GNOME Terminal、Linuxのような組み合わせでは、前の3要素は比較的よく知られており、一部の動作はPOSIXで標準化されている
- 個々のプログラムは何でもできるように見えるが、実際の利用体験ではかなり一貫したやり方で動作している
標準は少ないが慣習はある
- ターミナルプログラムの動作を包括的に定める実質的な標準はほとんどない
- 近い参考資料としては次の2つがある
- これらのルールは、プログラム作者が必ず従うべき規範というより、20年にわたってターミナルを使う中で観察された技術的慣習に近い
- 例外は多く、例外にはしばしばもっともな理由がある
ある種のルールはプログラム側で直接実装する必要がある
- 設定ファイルの場所、
--helpの出力、通常出力はstdoutへ、エラーはstderrへ書くといった挙動は、プログラム側の責任が比較的明確
- 一方で、自然に見える動作の中にも、実際にはプログラムまたは入力ライブラリが直接処理しなければならないものがある
- 代表例が、
Ctrl-DでREPLを終了する動作
catでは特別な実装がなくても、cooked modeでオペレーティングシステムがEOFを返す
ipythonではprompt-toolkitのコードでCtrl-D処理を実装している
- こうした実装責任を理解すると、プログラムごとに細かな挙動が少しずつ違う理由にも納得しやすくなる
ルール1: 非対話型プログラムはCtrl-Cで終了する
- 非対話型プログラムは、一般に
Ctrl-Cを押すと終了する
- プログラムが
SIGINTシグナルハンドラを設定していなければ、Ctrl-Cでデフォルトでは終了するため
- このルールは、
python3、bc、lessのような対話型プログラムにはそのまま当てはまらない
- 対話型プログラムでは、
Ctrl-Cはプログラム全体の終了ではなく、現在実行中の作業を中断することがある
- 例として、
lessでの検索や、python3で実行中のPythonコードがある
ipythonが使う入力処理ライブラリprompt-toolkitには、Ctrl-Cで検索を中断するコードがある
ルール2: TUIはたいていqで終了する
lessやhtopのようなTUIプログラムは、たいていqを押すと終了する
- このルールは、
qで終了するのが自然ではないプログラムには当てはまらない
ルール3: REPLは空行でCtrl-Dを押すと終了する
python3やedのようなREPLは、たいてい空行でCtrl-Dを押すと終了する
- cooked modeで
catのようなプログラムを実行すると、オペレーティングシステムは空行でのCtrl-Dに対してEOFを返す
sqlite3、python3、fish、bashなどは実際にはcooked modeを使っていないが、この基本動作をまねるためにこのショートカットを実装している
- 実装例は次の2か所で確認できる
- ErlangのREPLは
Ctrl-Dで終了しない例外
ルール4: 基本の16色を超えない
- ターミナルプログラムは、おおむね基本の16色のANSIカラー以外の色をあまり使わない
- hexカラーを直接指定すると、ユーザーの背景色と衝突する可能性が高い
- たとえば
#EEEEEEの文字は、白い背景ではほとんど見えないが、暗い背景では問題ないかもしれない
- 基本の16色を使えば、ユーザーがターミナルエミュレータ側で背景色に合うよう色を設定している可能性が高い
- この方法は、ターミナルエミュレータが対応している色に関する前提も減らせる
- 例外としてテキストエディタがある
- HelixはデフォルトでANSI基本色ではない紫の背景を使う
- Helixは「中核」プログラムではなく、配色が気に入らないユーザーはテーマを変更できるという考え方
ルール5: readlineのキーバインドをある程度サポートする
- 意味がある場面では、ほとんどすべてのプログラムがreadlineのキーバインドをある程度サポートしている
- 多くのプログラムは
readline自体を直接使ってはいないが、emacs/readline風のキーバインドをまねている
- すべてのプログラムがまったく同じ形でまねているわけではない
- atuinは
Ctrl-Aをprefixとして使っているようで、そのためCtrl-Aで行頭に移動しない
- これらのプログラムは独自のカット&ペースト用バッファを実装し、
Ctrl-Uで行を削除してからCtrl-Yで貼り付けられる
- 例外もある
git、cat、ncのようなプログラムは、backspace、Ctrl-W、Ctrl-Uを除けば行編集をサポートしない
- テキストエディタはそれぞれ独自の編集方式を持つ
- 関連する話題は、entering text in the terminal is complicatedでさらに詳しく扱われている
ルール5.1: Ctrl-Wは最後の単語を削除する
- テキストエディタを除けば、
Ctrl-Wで最後の単語を削除しないプログラムは見たことがない
- cooked modeでは、オペレーティングシステムはデフォルトで
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で似たことができる
- 多くのプログラムは、意味がある場合にこの方式を実装しているようだが、例外はたくさんあり得る
ルールを身につけるのに時間がかかる理由
- こうしたルールは、一度に明示的に学ぶというより、いくつもの段階を経て身につく
- 「
Ctrl-Cはプログラムを終了する」のように、まずルールの存在そのものを知る
findは終了するがlessは終了しない、といった例外に出会う
- 非対話型プログラムでは終了し、対話型プログラムでは現在の作業だけを中断することがある、というパターンを無意識に把握する
- その後になって初めて、明示的なルールとして整理できる
- ターミナルに関する多くの理解は、いまもなお無意識のパターン認識の段階にとどまっている
- ルールを文章としてまとめることで、他の人もターミナルの動作をもう少し速く学べるようになる
1件のコメント
Hacker News のコメント
さらに、ターミナルプログラムはユーザーのデフォルトの前景色/背景色を尊重し、理由なく変更すべきではない
色を使うとしても、デフォルトの背景/前景やターミナルのカラーパレットが何であれ読める必要があり、色だけで意味を伝えると、コピー/ペーストや色非対応のターミナルでは失われてしまう
絵文字や不要な非 ASCII 文字も慎重に使うべきで、スクロールされる標準出力の形式では、一時的に表示した警告やファイル名のような重要な情報を消さないほうがよい
派手な色やアニメーションを多用したいなら、少なくとも簡単にオフにできる設定オプションを提供すべき
根本原因が修正されたのかは分からないが、いくつものオープンソースプロジェクトで、本来の芸術的意図を少し諦める回避策の PR を作ることになった: https://github.com/docker-archive/toolbox/issues/695
ただし、ユーザー指定色を提供することはできる
おおむね、すべてのターミナルプログラムがすでに従っていると仮定できるものを整理したものに近い
最近は一部の作者が、ユーザーがパッチ済みフォントを使っていると仮定してツールを作り、ユーザビリティを大きく損なうこともある
良い記事だ
こうした慣例を学ぶのは難しいと述べていたので、CLI について考えるときに好んで参照している Command Line Interface Guidelines も勧めたい: https://clig.dev
Ctrl-C で終了する、標準入力で
-を受け付ける、パイプでは色をオフにするといった原文のルールと、さらに多くの内容を含んでいるPOSIX、GNU、Unix の資料、Heroku CLI ガイド、12-factor CLI アプリガイドが含まれている
「通常の出力は stdout に、エラーは stderr に出力すべき」という点は本当に重要だ
標準出力は、プログラムが生成するよう求められたデータだけのための場所であるべきだ。ユーザーが JSON データを要求したなら、標準出力にはその JSON オブジェクトだけが正確に存在すべきで、他のものは絶対に入ってはいけない
標準エラーという名前は実のところあまり適切ではなく、ターミナルでユーザーが読むべきすべてのメッセージを送る「標準ユーザーストリーム」に近い。エラーメッセージだけでなく、ステータスメッセージや詳細出力もここに含まれる
そうすれば、プログラムの出力を他のプログラムへ滑らかにパイプでき、データではない情報は引き続きターミナルへ送ることも、別の場所へリダイレクトすることもできる
プログラムが特定目的のターミナル接続ファイルディスクリプタを簡単に新規作成でき、終了コードのようにマニュアルで番号を文書化できるとよいと思う。互換性のためにデフォルトでは stdout や stderr に送れるようにしておき、このあたりを少し試してみるつもりだ
6つの異なるストリームがあり、それぞれを個別に捕捉したり別々に設定したりできる: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_output_streams?view=powershell-7.4
grepのような処理が動作せず、混乱を招くことがあるいくつか追加すると、readline/emacs のキーバインドである Ctrl-E、Ctrl-W を初めて聞くなら、ほとんどの macOS 入力ソースがこのキーバインドを使っていることを知っておくとよい
macOS なら、今ブラウザのアドレスバーで Ctrl-E、Ctrl-W、Ctrl-U を試せる
行編集をまったくサポートしていないコマンドラインプログラムを使うなら、
rlwrapをインストールして、その下で REPL を実行できる。たとえば Standard ML of New Jersey は REPL はあるが行編集機能がないので、rlwrap smlnjで補完できる「16色より多く使うな」という話については、さらに進めて「8色より多く使わないか、少なくとも色を設定可能にせよ」と言いたい。Solarized やデフォルトの Base 16 のような人気のカラーパレットは、「明るい」色を複数段階のグレーに使うこともあるため、作者が想定した明るい緑が、実際には通常テキストと同じグレーである可能性がある
私が使っているターミナルエミュレータ Alacritty もそうだし、タブ付きのファイルエクスプローラの大半も同じように動作する。記憶では Windows Explorer も今はそうだった気がするが、Windows を使わなくなってかなり経つ
REPLでCtrl-Dで終了する慣習は、GHCiではいつも私を悩ませる
たいていCtrl-Dを押して、何も起きなくて戸惑ったあと、GHCiでは効かないことを思い出して
:qを実行するするとCtrl-Dが入力の先頭に見えない文字を入れてしまい、“lexical error at character '\EOT'”エラーになり、見えない接頭辞なしで改めて
:qを入力しないとGHCiが終了しないほかの方法が何なのかもよく分からないが、この挙動を変える設定があるのだろうか?
抜けているものの一つは、
~をホームディレクトリとして扱う挙動これはPOSIX APIではなくシェル機能に見える。Goで
os.ReadFile("~/.bashrc")は動かないが、シェルではecho ~/~/~が/home/jrockway/~/~のように展開されるyourprogram ~/path/to/fileは常に動くが、ファイル名を尋ねるREPLでは動かない場合がある。実際には多くのソフトウェアがこれをサポートしているので、「ほとんどのTUIプログラムがすること」に入れてよいと思う単語展開ではチルダ展開が最初の段階で、その後にフィールド分割、パス名展開、引用符の削除が続く
関連するシェル仕様はhttps://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06にあり、仕様全体はhttps://pubs.opengroup.org/onlinepubs/9799919799/で見られる
ツールがチルダ展開を追加すると、現在のユーザーのホームディレクトリを知る必要があって実装が複雑になり、
~だけ対応して~nameは対応しないような不完全な実装になりかねないPOSIXシェルでは
~nameはそのユーザーのホームディレクトリに展開されるので、システムユーザーまで含めて任意のユーザーのホームディレクトリを検索する必要がある。NSS設定によっては/etc/passwdを読むよりはるかに複雑になり得る対話型TUIならチルダ展開を実装するのは有用かもしれないが、その場合はシェルと同じくらい完全であるべきだ。CLIプログラムはこうした魔法のような展開をしないほうがよい
"~"という名前のファイルがあったらどうするか、"~username"はどうするか、どうエスケープ/引用するか、$HOMEがなかったり実際のホームと違ったりしたらどうするか、Windowsではどうするか、という問題が出てくるあまり知られていない事実として、sshでは改行の後に
"~."を入力するとクライアント側で接続を強制的に閉じ、サーバーは何もできないので、ワークフローに組み込まないほうがよい./myprogram --config=~/.config/myprogramのような形の場合もちろん多くのフラグパーサーは、シェルが展開してくれる形である
./myprogram --config ~/.config/myprogramを使えるので、これを知っているか偶然サポートしていることになるだが筋肉記憶のせいで必ず
=を入力してしまう。以前の職場のフラグ解析ライブラリでついた習慣のようで、同じ理由で--hyphens-like-normal-peopleの代わりに--underscores_like_thisもいつも使っている特に
..はOS APIレベルで処理されるので、POSIXでも~がそうなると期待しがちCLIをGUIや、時にはTUIよりもずっと好む主な理由は、はるかに一貫していると感じるから
慣習があり、CLIであらゆる慣習に従うのは、良いGUIを設計するよりはるかに簡単だ。そのため結果として品質が高くなる傾向がある
この性質をGUIに持ち込む方法をよく考えるが、今のところ最良の答えは「多くの努力」か「期待値を下げる」くらい
ほかのOSはGUIショートカット用のメタキーを定着させられず、
ctrl-cのような衝突がはるかに多いmacOSネイティブアプリの基盤となるアプリケーションフレームワークのデフォルトは、共通のキーボード操作を多く提供している。標準のメニューバークラスを使い、標準コマンドを使えば、Command O、N、Q、X、C、Vなどがほぼ無料で付いてきて、標準のテキストフィールドを使えばCommand/Option/Fn + 矢印キーのナビゲーションも自動的に付く
Windowsもこの点ではかなり良いが、基本の修飾キーがCtrlであることがターミナルベースのアプリの使用を妨げることがあり、OS内部の複数のUIフレームワークがそれぞれ異なるデフォルトを持っているように見える
Linuxの世界では、誰かが完全なアプリケーションフレームワークを設計し、ディストリビューションが標準化してアプリを移植しないと実現しなさそうだ。ウィンドウマネージャーはウィンドウ内の挙動を指示したがらず、GUIツールキットはOS全体のデフォルトを定義したがらず、デスクトップ環境やディストリビューションもUIフレームワークを強制したり移植したりする役割を担いたがらない
プログラムはホームディレクトリにファイルを追加せず、XDG_CONFIG_HOMEのような設定を尊重すべき
アプリケーションがどう振る舞うべきかを説得しようとしているのではなく、アプリケーションでよく見かける挙動を列挙している記事だ
「Ctrl-W は最後の単語を削除すべき」という規則の例外として mysql(1) がある
mysql は readline ではなく editline にリンクされているため、デフォルトでは Ctrl-W が最後の単語ではなく行頭まで全部削除してしまう
しばらくこれを使わなければならなかったとき、丹念に書いたクエリが丸ごと消えて本当に気が狂いそうだった
大学時代に Oracle データベースへ接続しようとして sqlplus を使ってみるという不運があったのだが、Ctrl-W のようなターミナルのショートカットがまったく効かないだけでなく、行内でカーソルを前に移動することも、前のコマンドへ戻る履歴検索もなかった
タイプミスが一つでもあると全体を最初から入力し直さなければならず、
/を一つ入力すれば最後のコマンドをそのまま実行することはできたが、タイプミスの修正には役に立たなかったCtrl-L で画面をクリアすることもできず、代わりに
clear scrを直接実行する必要があった。コマンドを単にclearにするのは、あまりに分かりやすすぎたのだろう長時間実行プロセスは SIGHUP を受け取ったら設定を再読み込みすべき、という項目も追加したい
その違いは大きく、残念ながらこの項目は元のリストに入るほど普遍的ではない
SIGHUP に反応する長時間実行プロセスは通常、制御 TTY なしで切り離されて実行されるため、「ターミナルプログラム」と呼ぶにはやや無理がある
アプリケーションが設定の状態だけでなく、設定から派生した状態まで誤ってキャッシュしないよう注意する必要があり、そうしないと互いに食い違ってしまう
単純なアプリでは簡単かもしれないが、設定ファイルが必要なアプリの多くはそこまで単純ではない