- コマンドラインでテキストを編集するのは基本機能のように見えるが、プログラムごとに入力処理が異なるため、同じキーがまったく違う動作をすることがある
- ターミナルでの入力体験は、大きく分けて入力システムなし、GNU readline、
libedit のような代替ライブラリ、独自入力システムの4つの流れに分かれる
cat、nc、dash のような単純なツールでは、矢印キーが ^[[D のように表示されることがあるが、ターミナルが提供する Ctrl+W、Ctrl+U、Ctrl+C、Ctrl+Z は引き続き使える
bash、psql、irb、python3 など readline ベースのツールは、履歴、矢印キー、Ctrl+R 検索、Ctrl+A/Ctrl+E 移動を提供し、rlwrap で似た体験を追加することもできる
- 現在の入力システムを見分けられると、矢印キーや
Ctrl+R がなぜ違って見えるのかを素早く判断でき、必要に応じてドキュメント確認や rlwrap のような回避策を選びやすくなる
プログラムごとに変わる入力体験
- ターミナルでテキストを入力して修正する方法は、プログラムごとに大きく異なる
- 代表的なタイプは次のように分かれる
cat、nc、git commit --interactive などは矢印キーをサポートしておらず、^[[D^[[D^[[C^[[C^ のような文字列が見えることがある
irb、Linux の python3 などは readline ライブラリによって、履歴や矢印キーといった基本的な編集機能を提供する
- Mac の
/usr/bin/python3 のように、矢印キーは動作しても Ctrl+left や Ctrl+R の逆方向検索には対応していない場合もある
fish、ipython3、micro、vim などは独自の入力システムを使っている
モード1: 入力システムのない基本状態
- プログラムが
fgets() のような呼び出しでテキストだけを受け取り、別個の編集機能を提供していない場合、矢印キーは編集コマンドではなくエスケープシーケンスとして出力されることがある
- たとえば
dash で左矢印を押すと、次のように ^[[D がターミナルに表示される
$ ls l-^[[D^[[D^[[D
- それでも、ターミナルが標準で提供する制御機能は残っている
- テキスト入力
- バックスペース
Ctrl+W: 直前の単語を削除
Ctrl+U: 行全体を削除
Ctrl+C: プロセスを割り込み
Ctrl+Z: プロセスを一時停止
- このような環境でも
Ctrl+W を使えば、バックスペースを何度も押さなくても単語単位で消せる
- ターミナルが対応する制御コードは
stty -a で確認できる
モード2: readline を使うツール
- readline はテキスト入力をより快適にする GNU のライブラリで、多くの対話型ツールで使われている
- よく使う readline ショートカットは次のとおり
Ctrl+E または End: 行末へ移動
Ctrl+A または Home: 行頭へ移動
Ctrl+left/right arrow: 単語単位で前後に移動
- 上矢印: 前のコマンドへ移動
Ctrl+R: 履歴検索
- 基本モードの
Ctrl+W、Ctrl+U も使えるが、readline では Ctrl+U は行全体ではなく、カーソル位置から行頭までを削除する
bash の Ctrl+R 履歴検索は readline が提供している機能である
psql、irb、python3 なども readline を使うプログラムの例である
- readline の編集コマンド一覧は readline マニュアル で見られる
readline のないツールに rlwrap を付ける
nc のように readline をサポートしていないプログラムでも、rlwrap nc として実行すれば readline 対応プログラムのように使える
- rlwrap は、readline のないツールの使い勝手を大きく向上させられる
- ユーザー定義の自動補完も設定できるが、実際に試した内容ではない
ツールが readline を使わない理由
- 一部のツールが readline を使わないのには、いくつか理由がある
cat、nc のような非常に単純なプログラムは、比較的大きな依存関係を追加したくないことがある
- readline は LGPL ではなく GPL ライセンス なので、プログラムのライセンスと適合しないことがある
- プログラムの一部だけが対話的で、入力が
y や n のような単一文字に近いなら、readline 対応の優先度が低くなることがある
git には git add -p のような対話機能があるが、意味のある長い入力が必要なときは通常テキストエディタを起動する
idris2 は依存関係を最小限にするため readline を使っておらず、よりよい対話機能が必要なら rlwrap の使用を勧めている
現在 readline を使っているか確認する方法
- もっとも簡単な確認方法は
Ctrl+R を押してみること
- 次のプロンプトが表示されれば、readline を使っている可能性が高い
(reverse-i-search)`':
- 他のライブラリでも同じ用語を使う可能性はあるため決定的な判別法ではないが、履歴検索に
reverse-i-search という表現を使う別のシステムは知られていない
readline ショートカットと Emacs
Ctrl+A で行頭、Ctrl+E で行末に移動するショートカットは、Emacs 由来と考えられる
- Emacs でも
Ctrl+A と Ctrl+E は readline と同じ動作をする
- 一方で
Ctrl+W、Ctrl+U は Emacs とターミナルで同じ動作にはならない
- readline プロジェクトの歴史は Readline プロジェクトの歴史 でさらに読める
モード3: libedit のような代替入力ライブラリ
- Mac の
/usr/bin/python3 は、一部の readline 機能だけをサポートする中間的な状態に近い
- たとえば
Ctrl+left arrow を押すと、単語移動ではなく ;5D が表示されることがある
$ python3
>>> importt subprocess;5D
- Mac OS 標準の Python インストールでは、Python の
readline モジュールは実際には libedit ベースである
- これは readline が GPL ライセンスであるためだと推測される
- 次のコマンドで、その Python が libedit を使っているか確認できる
$ python3 -c "import readline; print(readline.__doc__)"
Importing this module enables command line editing using libedit readline.
- Linux や Homebrew でインストールした Python は一般に readline を使う
- Python 3.13 は readline 依存を取り除き、独自ライブラリを使う予定のため、今後は「Python は readline を使う」という説明が常に正しいとは限らなくなるかもしれない
モード4: 独自入力システム
- 一部のプログラムは独自のテキスト編集システムを使っており、場合によっては readline より多くの機能を提供する
- 独自入力システムを使う例は次のとおり
nano、micro、vim、emacs のような大半のターミナル向けテキストエディタ
fish のようなシェル
zsh の行編集機能 zle
ipython のような REPL
atuin のようなツール
- このようなシステムでは、次のような機能が見られる
- ツールにより最適化された自動補完
- 構文ハイライトを含む、よりよい履歴管理
- より多くのキーボードショートカット
readline に着想を得た独自入力システム
- Atuin はシェル履歴検索ツールで、入力実装は独自だが readline に着想を得ている
- Atuin は、ユーザーが readline のキーバインドに慣れている点を活かし、readline を直接使わなくてもそのキーバインドが動作するようにしている
- IPython が使う prompt_toolkit も複数のオプションをサポートしており、vi スタイルのキーバインドも含まれる
- prompt_toolkit のデフォルトは readline スタイルのキーバインド対応である
- これは、多くのプログラムが
j を下、k を上に使う基本的な vim キーバインドをサポートしているのと似ている
- たとえば Fastmail は、ほとんどのショートカットが vim と特に関係ないものの、
j、k はサポートしている
- readline に着想を得た独自入力システムは readline と微妙に互換性がないこともあるが、基本的なショートカットをいくつか使うだけのユーザーには大きな問題にならないかもしれない
- このような独自システムは、単に readline を使うより自動補完が優れていることが多い
vi モードをサポートするシェル
bash、zsh、fish はいずれもテキスト入力用の vi モード をサポートしている
- Mastodon で行われた非科学的アンケートでは、12% が vi モードを使っていると答えた
- readline 自体にも vi モードがあり、
bash の vi モード対応はこれによって実現されている
- readline を使う他の多くのプログラムにも vi モードがある可能性がある
入力状態を診断する流れ
- どの入力システムを使っているか分かると、コマンドラインの挙動をより予測しやすくなる
- 入力プロンプトでは、次の順序で判断できる
- 矢印キーが動作しないなら、入力システムがない可能性が高い。それでも
Ctrl+W、Ctrl+U は使え、必要なら rlwrap を付けられる
Ctrl+R で reverse-i-search が出れば readline の可能性が高く、見慣れた readline ショートカットと基本的な履歴機能を使える
Ctrl+R が別の動作をするなら、独自入力ライブラリの可能性が高く、たいていは readline に近い動作をし、必要ならドキュメントを確認すればよい
- ここで扱っていない入力まわりの複雑さも残っている
ssh、tmux 関連の問題
TERM 環境変数
- GNOME Terminal、iTerm、xterm など端末ごとのコピー・ペースト対応の違い
- Unicode
- その他さまざまな入力関連の問題
1件のコメント
Hacker Newsの意見
Juliaの記事はいつも良い。補足すると、シェルスクリプト内で
sttyを使えば、入力処理の方式を含め、端末のさまざまな挙動を変更できる。以前
shとsttyで試した実験: https://gist.github.com/alganet/63f1dbc97b8fd35f7bb14ec30f79...VT100互換端末(mintty、xterm、Terminal.app、vscodeなど)では、ほとんどのキー組み合わせやマウスジェスチャ(押下/ドラッグ/離す)まで、シェル内でキャプチャして解釈できる。
実行デモは
bash -c "$(curl -L https://git.io/fjToH)"で、キーを押したりマウスを動かしたりすればよい。終了は Ctrl+W。zsh と ksh も対応しているが、read -rn1のない dash などは対応していない。また
vi | cat -vのように対話型プログラムをcat -vにパイプすると、そのプログラムが使う VT100エスケープを見られる。vi がどうしているのかを見て多くを学んだ。stty saneと入力すれば元に戻せる。端末を3つ開き、1)
man ascii、2)nc -lvp 9001 | xxd -c1、3)stty raw -echo; nc -nv 127.0.0.1 9001を実行したうえで、3番目の端末から manpage の「Hex」列全体を順番に再現してみればよい。値によっては複数の方法で作れ、ほかのキーの一部は 複数バイトのペイロードを送ることも観察できる。
sttyと raw モードの理解に役立った: https://viewsourcecode.org/snaptoken/kilo/, https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode...魔法のように見える
sttyのパラメータや記号は、昔に定義された制御シーケンスに由来する。ECMA-48 のような1970年代の曖昧な標準には、今ではほとんど使われない難解な用語も多い。たとえば Select Graphic Rendition(SGR) は、おおよそ1970年代版のHTMLタグのようなもので、前景色の設定には
ESC[38;2;R;G;BmTextESC[0mのような形式を使う。ESC[は CSI、mは SGR、38は前景色、2は RGB色空間、0はスタイルのリセットである。コマンドの前にパラメータが来る構造なので、少し逆に感じる。ブラウザと同じく、端末も実装範囲や方式はまちまちで、今もそうだ。色はかなりよくサポートされているが、ほかの機能はそうではない。たとえば
SGR<7>は「negative image」モードだというが、標準が詳しく説明していないため、ある端末は反転表示し、別の端末は別の方法で処理しており、今でも問題が起きる。20年以上前に readline の上に状態機械を作り、1行が折り返される程度ではなく、上下に移動できる本物の複数行エディタとして使えるようにした。
動画: https://github.com/colmmacc/jot/raw/master/jot-demo.mp4
この機能が入った Unix 端末IMクライアントの CVS リポジトリ: https://c-hey.redbrick.dcu.ie/src/c-hey_cvs_latest/
行間を上下に移動するときにカーソルを追跡して再描画し、端末サイズが変わると SIGWINCH を見て描き直す。Rust で書き直して小さなライブラリとして公開したいが、時間がなかった。誰も似たようなものを作っていないことには、いつも驚いていた。
autoload -Uz zedの後にzedを使って一時ファイルを編集し、git commit -Fに渡せば、ZLE の全機能を備えたインラインエディタを起動できる。C-x C-wで保存してコミットし、C-cでキャンセルすればよい。この基本実装の利点は、
zedが新しい ZLE ウィジェットなのでキーマップを完全に制御できる点だ。emacs モードや vi モードがそのまま動作し、さらにカスタマイズもできる。自分で ZLE ウィジェットを書けば、行エディタ内から直接呼び出せるので、上のようにコマンドをラップする必要もなくなる。
https://github.com/zsh-users/zsh/blob/master/Functions/Misc/...
git commitの対話モードのようにコンテキスト切り替えをしなくてよい利点にも共感する。「インラインタイプライター」のような効果も格好いい。記事で触れられていない点としては、幅広文字、キーボードモードの違いにより同じキー入力が異なる ANSI エスケープシーケンスで表される問題、ローカルエコーのような TTY 状態の違い、TTY 状態を変更するシステムコールが OS ごとに微妙に異なる点がある。
端末エミュレーションの対応もまちまちだ。最近は概ね xterm をまねているが、その中でも 100% 互換を目指しているものはない。
端末が提供する機能を確認する方法についても合意が不足している。ある側は ANSI エスケープシーケンスを送って応答を待ち、ある側は
$TERMを見て、ある端末は自分を xterm として公開しつつ VT 機能シーケンスは無視して別の環境変数を設定する。正直、ユーザーエージェント文字列よりさらにひどい。これはすべて POSIX システムという前提での話で、Windows まで混ぜると面白さが倍増する。
bash で
$EDITORを好みのエディタに設定しておけば、Ctrl-X Ctrl-E で現在の行を$EDITORに送れる。コマンドを編集してから保存して終了すると実行される。vを押してもよい。fc組み込みコマンドも同じことをする。Ctrl キーシーケンスなしで、ただfcと入力すればよい。直前のコマンドをデフォルトの
$EDITORではなく特定のエディタで開くこともできる。たとえばEDITOR=viのとき直前のコマンドを ed で編集したいなら、fc -e edを使えばよい。vi モードでコマンド履歴から素早くシェルスクリプトを作る方法もある。目的のコマンドが履歴の 15 番目なら、
fc 15のあとw1.sh、%d、wqのように処理できる。EDITOR=nvimと copilot.vim を組み合わせて、AI シェル補完のように使っている。かなり良い。ここで見た: https://twitter.com/arjie/status/1575201117595926530?s=46&t=...
Linux 初心者の立場では、プログラムが
fgets()のようなものでテキストを読むだけでも、バックスペース、Ctrl+W、Ctrl+U のような機能が無料で付いてくる仕組みが気になる。fgets()のドキュメントには改行または EOF が現れるまで読むと書かれているが、基本的にユーザーが改行を入力するまでブロックされ、その前に端末が行バッファをバックスペースや Ctrl+W/Ctrl+U で編集できるようにしてくれる、という意味なのか気になる。しかしプログラムが必ず 1 行全体を読む保証はなく、
countに 2 を指定して文字単位で読むこともできる。そうするとバックスペースが発生したときに、すでに読んだ文字を「戻す」ことはできないはずだが、fgets()の内部バッファに何か魔法があるのか、それともそういう場合は行編集機能が壊れるのか気になる。https://en.cppreference.com/w/c/io/fgets
改行単位でバッファリングされているように見えるのは実際にはシェルが管理しているからで、そのため開発者はタブ補完や行頭に sudo を付けるといった テキストバッファ操作 機能を入れられる。
Enter を押す前に画面に表示されている文字は端末のメモリにあるだけで、アプリケーションの
readシステムコールは端末ファイルディスクリプタでブロックされている。行が完成するまで端末はデータを書き込まない。このとき入力中の行編集も通常は端末自体が提供する。文字が画面に見えるのもエコーという端末機能であり、バックスペース、削除、SIGINT 用の Ctrl+C、EOF 用の Ctrl+D も同様だ。
行エディタやテキストエディタはこうした機能をオフにし、raw モード(raw mode) にする。入力したものはすべて即座にアプリケーションへ送られ、アプリケーション側で直接処理される。パスワードプロンプトが文字を隠すためにエコーだけをオフにするのもよくある例だ。
伝統的な Unix の行末
\nも実は端末機能だ。端末から見ると\nはカーソルを 1 行下へ移動するだけなので、行頭に戻るには\rも必要になる。本当の行末は\r\nであり、端末が見えないところで\nを\r\nに変換してくれている。これも無効化できる。raw モードではこのようには動作しない。
helpプログラムを SunOS に移植することだった。最初の難関は Enter/Return を待たずにキー入力ごとに反応する方法だった。上司は意図的にほとんど助けてくれず、Web もなく O’Reilly の本もほとんどなかった 1986 年に、ほぼ一人で解明させた。今でもその判断に永遠に感謝している。
dash シェルも libedit 付きでコンパイルすれば編集モードをサポートする。Debian 系はおそらく容量の問題でそうしていないが、bash から始めた人たちがあとで学ぶことを少なくするには、サポートしたほうがよいと思う。
POSIX 標準も
set -o viを任意拡張として指定している。POSIX 互換を主張するシェルがこの編集モードを実装するなら、必ずset -o viをサポートしなければならない。「[set -o] vi: 組み込みの vi エディタを使用してシェルのコマンドライン編集を許可する。vi モードを有効にした場合、実装拡張として提供される他のコマンドライン編集モードは無効化しなければならない。」
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V...
ターミナルは、Linuxを永遠に市場シェア5%未満に縛りつけている単一の要因だとずっと考えてきた
テキスト入力だけが問題なのではなく、体験全体が複雑。人口の95%はただドローンを飛ばしたいだけなのに、ラベルもないコンコルドの操縦席を突きつけているようなもの
https://qph.cf2.quoracdn.net/main-qimg-2566f4c91b894e4169d77..., https://media.thedroningcompany.com/images/tincy/WQZpC56vqMp...
Ubuntuの裏側にコンコルドの操縦席があり、仕事用のUbuntuノートPCからそこにアクセスできる点は、むしろありがたい。そのレベルの制御権がないのは嫌だと思う
一方で母のノートPCもUbuntuで、彼女のノートPCはManjaroだが、どちらもコンコルドの操縦席には足を踏み入れず、「ドローンを飛ばす」だけで問題なく使っている。もちろん自分はArchを使っている
ターミナルは開発者とシステム管理者のためのツール。デスクトップGNU/Linuxユーザーの大半が開発者やシステム管理者だからそう見えるだけで、彼らは人口の数パーセントにすぎない
むしろ奇妙なショートカットをたくさん知らなければならないのはmacOSのターミナルで、すべてが可能な限り直感的でないように作られている
おそらく不人気な意見だろうが、iTerm2でNatural text editingプリセットに切り替え、従来のmacOSの編集ショートカットに合わせている
ターミナルアプリ内では通常、制御シーケンスに再マッピングしてくれるため、複数の場所で変更したりreadline対応を心配したりしなくても、ほぼどこでも機能を得られる利点がある
完璧ではなく、ときどき手動で入力すべきシーケンスまで再マッピングされることがあったが、筋肉記憶に合っているので、コマンド編集をずっと多く使うようになった
https://pliszko.com/blog/post/2021-10-31-natural-text-editin...
ターミナルのテキスト編集に特別な動作をしてほしくない
知っていれば生活がよくなる基本的なreadlineキーバインドは、Ctrl+W、Ctrl+O、Ctrl+Rの3つ
Ctrl+WはJuliaが記事で触れているように、最後の単語を削除する
Ctrl+Oは履歴から呼び出した行を実行し、その次の履歴行を再び呼び出す。履歴内の最初のコマンドに移動してからCtrl+Oを5回押すと、5つのコマンドを順番に実行できる
Ctrl+Rは検索文字列を入力しながら履歴を逆方向に検索する。もう一度Ctrl+Rを押すと前の結果へ、Ctrl+Sは時間的に先へ移動する。EnterまたはCtrl+Oで実行する
前方検索機能にそのキーの組み合わせを使うには、なぜ動かないのかを理解し、XOFFをオフにする意思も必要になる
Windows Terminal では Ctrl+C が常に適切に動作する。テキストを選択中ならコピーし、選択中でなければ実行中のプロセスを終了する
Linux のターミナルアプリは Ctrl+C でコピーせず、Ctrl+Shift+C を要求する。貼り付けも Ctrl+V を押すと変な文字が入るし、他のアプリはどれも Ctrl+V なので、筋肉記憶のせいで何度も引っかかる
Windows Terminal のように動作して、「不適切なテレタイプの使い方」だと手の甲を叩かれない Linux ターミナルアプリがあるのか気になる
この方式は別の Xorg バッファを使うため、Ctrl+C で何かをコピーした後に別のテキストを選択しても、Ctrl+V は最初のものを貼り付け、中クリックは二つ目を貼り付ける
Windows やスマートフォンでは、何かを選択しただけで Ctrl+C のような操作を押すのを忘れてしまい、いらいらすることがある
Linux ターミナルで Windows Terminal のようにするものはなさそうだ。ターミナル内で実行中のプログラムが Ctrl+C を直接処理したい場合があるためだ。テキストエディタがその例で、ターミナルは現在どのプログラムが実行中かも知らない。ジョブ制御はシェルの仕事だからだ
map ctrl+c copy_or_interruptを使えばよいhttps://sw.kovidgoyal.net/kitty/actions/#action-copy_or_inte...