こんにちは! 私は最近ターミナルのことをよく考えていて、昨日は Ctrl-ACtrl-CCtrl-W などの「制御コード」について気になりました。これらは一体何なのでしょうか?

ASCII制御文字の表

以下は、私のマシン(Mac OS)上で、33個のASCII制御文字すべてとその動作を大まかにまとめた表です。数多くの注意点がありますが、それらについても後で説明します。

ascii-control.png

この表は HTMLページ としても閲覧できます(RSSで表示させるために画像にしています)。

異なる種類のコードが混在している

この図で最初に驚いたのは、33個の制御コードが(大まかに)以下のカテゴリに分かれていることです:

  1. オペレーティングシステムの端末ドライバが処理するコード。たとえばOSが 3Ctrl-C)を受け取ると、現在のプログラムに SIGINT シグナルを送信します。
  2. その他のコードはアプリケーションにそのまま渡され、アプリケーションが自由に扱えます。内訳は以下の通りです:
    • キーボード上のキーの実際のキー押下に対応するコード(EnterTabBackspace)。たとえば Enter を押すと端末に 13 が送信されます。
    • readline が使用するコード:「アプリケーションが自由に扱える」とは、実際には「readline ライブラリがやるのとほぼ同じことをする(アプリケーションが実際に readline を使っていなくても)」という意味であることが多いため、readline が使用するコードをいくつかラベル付けしました。
    • その他のコード。たとえば Ctrl-X は端末全体では標準的な意味を持ちませんが、emacs では非常に多用されます。

どのコードがどのカテゴリに属するかに本当の構造はなく、すべて有機的に進化した結果としてランダムに散らばっています。

(readlineについて詳しく知りたい場合は、ターミナルでのテキスト入力は複雑という記事を書いています。また、チートシートも多数存在します)

制御コードは33個しかない

もう一つ驚いたのは、制御コードが33個しかないことです。A〜Zに加えて7つ(@, [, \, ], ^, _, ?)です。つまり、端末アプリケーションで Ctrl-1 をショートカットキーとして使いたい場合、それは実質的に意味がありません。私のマシンでは Ctrl-1 は単に 1 を押したのと同じで、Ctrl-3Ctrl-[ と同じです。

また、Ctrl+Shift+C は制御コードではありません。その動作は端末エミュレータによって異なります。Linuxでは Ctrl-Shift-X は端末エミュレータがコピーや新しいタブの作成、貼り付けなどに使うことが多く、TTYには一切送信されません。

また、私はよく Ctrl+Left Arrow を使いますが、これも制御コードではなく、ANSIエスケープシーケンス(ctrl-[[1;5D)を送信します。これは別の仕組みで、この投稿では扱いきれません。

この「33個しかない」という事実は、GUIで Ctrl+KEY を任意のキーで使えるのとは全く異なります。

公式のASCII名はあまり意味がない

これらの33個の制御コードにはそれぞれASCIIでの名前があります(例: 3ETX)。これらの制御コードが最初に定義されたとき、コンピュータや端末で使われることはなく、電信機で使われていました。電信機とUNIX端末は異なるため、多くのコードが別の意味に再利用されました。

個人的には、これらのASCII名はあまり役に立たないと感じます。なぜなら、半分のケースでASCIIの名前が現在のUNIXシステムでの実際の動作と関係がないからです。そのため、どの名前が元の意味と一致しているかを考えるより、ASCII名を完全に無視する方が簡単です。

Ctrl-M をショートカットキーとして使うのは難しい

もう一つの奇妙な点は、Ctrl-M が文字通り Enter と同じであり、Ctrl-ITab と同じであるため、これらをショートカットキーとして使いにくいということです。

簡単な調査によると、一部の人は依然として Ctrl-ICtrl-M をショートカットキーとして使っています()。しかし、そのためには端末エミュレータをデフォルトとは異なる扱いをするように設定する必要があります。

私の主な結論は、ターミナルアプリケーションを書く場合、ショートカットキーとして Ctrl-ICtrl-M を避けるべきだということです。

送信される制御コードを特定する方法

この記事を書くにあたり、さまざまなキー組み合わせが何をするかを調べるために実験を繰り返したので、echo-key.py というPythonスクリプトを書いて出力するようにしました。

もっと公式の方法もあるでしょうが、カスタマイズできるスクリプトがあるのは便利でした。

注意:正規モードと非正規モードについて

この表で Ctrl-WCtrl-U は「OSが処理する」とラベル付けされていますが、実際には常にOSが処理するわけではなく、ターミナルが「正規(canonical)」モードか「非正規(noncanonical)」モードかによって異なります。

正規モードでは、プログラムは Enter を押したときのみ入力を受け取り(OSが BackspaceCtrl-W を押したときの文字削除を担当)、非正規モードではキーを押した瞬間にプログラムが入力を受け取り、Ctrl-WCtrl-U のコードはプログラムにそのまま渡されて自由に処理されます。

非正規モードでも、プログラムは通常OSと同様に Ctrl-WCtrl-U を処理しますが、細かな違いがあります。

正規モードを使うプログラムの例:

  • おそらく grepcat などの非対話型プログラム
  • git(おそらく)

非正規モードを使うプログラムの例:

  • python3irb などのREPL
  • シェル
  • lessvim などのフルスクリーンTUI

注意:OS端末ドライバのコードはすべて stty で設定可能

Ctrl-CSIGINT を送信すると述べましたが、技術的には必ずしもそうではありません。必要であれば stty というツールを使って、「OS端末ドライバ」とラベル付けしたコードすべてとBackspaceを再マッピングできます。現在のマッピングは stty -a で確認できます。

私のマシンでの現在のマッピングは以下の通りです:

$ stty -a
cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;
	eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;
	min = 1; quit = ^\; reprint = ^R; start = ^Q; status = ^T;
	stop = ^S; susp = ^Z; time = 0; werase = ^W;

私はこれまでこれらのどれも再マッピングしたことはなく、その理由も想像できません(混乱と災厄の元になると思います)。しかし、Mastodonで尋ねたところ、人々が stty を使う主な理由は以下の通りでした:

  • stty sane で壊れたターミナルを修復する
  • stty erase ^H でBackspaceの動作を変更する
  • stty ixoff を設定する
  • 一部の人は SIGINTDELETE キーなどにマッピングする

注意:シグナルについて

シグナルに関する注意点が2つあります:

  1. ISIG 端末モードがオフになっている場合、OSはシグナルを送信しません。たとえば vimISIG をオフにします。
  2. BSDでは、追加の制御コード(Ctrl-T)があり、SIGINFO を送信します。

プログラムが設定している端末モードは、straceioctl システムコールを調べることで確認できます:

$ strace -tt -o out  vim
$ grep ioctl out | grep SET

vim が起動時に設定するモード(ISIGICANON が欠けています):

17:43:36.670636 ioctl(0, TCSETS, {c_iflag=IXANY|IMAXBEL|IUTF8,
c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST, c_cflag=B38400|CS8|CREAD,
c_lflag=ECHOK|ECHOCTL|ECHOKE|PENDIN, ...}) = 0

終了時にモードをリセットします:

17:43:38.027284 ioctl(0, TCSETS, {c_iflag=ICRNL|IXANY|IMAXBEL|IUTF8,
c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD,
c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE|PENDIN, ...}) = 0

vim が使用している特定のモードの組み合わせは「rawモード」と呼ばれる場合があります。man cfmakeraw を参照してください。

多くの競合がある

「33個しかない」ということに関連して、システムの異なる部分が同じコードを異なる目的で使おうとする競合がたくさんあります。たとえば、デフォルトでは Ctrl-S が画面をフリーズさせますが、それをオフにすると readline が前方検索に Ctrl-S を使います。

もう一つの例として、私のマシンでは Ctrl-TSIGINFO を送信したり、2文字を入れ替えたり、まったく別の動作をしたりすることがあり、以下によって異なります:

  • プログラムが ISIG を設定しているかどうか
  • プログラムが readline を使っているか、またはその動作を模倣しているかどうか

注意:「backspace」と「other backspace」について

この図ではコード127を「backspace」、8を「other backspace」とラベル付けしました。どういうことでしょうか?

Mastodonでの返信で最も議論になったトピックがこれで、非常に長い歴史があるようです。

まず、私のマシンでの動作は以下の通りです:

  1. Backspace キーを押す
  2. TTYにバイト 127(ASCIIでは DEL)が送信される
  3. OS端末ドライバとreadlineの両方が 127 を「backspace」にマッピングしている(正規モードと非正規モードの両方で動作する)
  4. 直前の文字が削除される

Ctrl+H を押した場合、readlineを使っていれば Backspace と同じ効果がありますが、readlineをサポートしていないプログラム(例:cat)では単に ^H と表示されます。

上記のステップ2は人によって異なり、Backspace キーが 127 ではなく 8 を送信する場合があり、その場合は sttyerase = ^H を設定する必要があります。

Debianポリシーマニュアルのキーボード設定に関するセクションには、Debianポリシーにおける DeleteBackspace の望ましい動作が記述されており、私のMacでの現在の動作と非常に似ています。このポリシーは90年代に書かれたもので、当時 Backspace の動作に関する混乱が多かったため、標準を設ける必要があったようです(このMastodon投稿経由)。

他にも端末に関する歴史的な話題はたくさんありますが、ここではこれで終わりにします。

この動作にはおそらくもっと多様性がある

「私のマシンでの動作」が他の人のマシンと異なる点は他にもたくさんあり、私のマシンでの動作についても誤りを犯している可能性があります。しかし、今日はこれで十分です。

省略した追加情報: stty -a によると、Ctrl-O は「discard」、Ctrl-R は「reprint」、Ctrl-Y は「dsusp」です。これらが実際に何をするのかはわからず(押しても明らかな動作はなく、歴史的に何をしていたかを教えてくれた人もいますが、2024年に有用かどうかは不明)、実際にはアプリケーションにそのまま渡されることが多いため、Ctrl-RCtrl-Yreadline とラベル付けしました。

これらのすべてが必ずしも役に立つわけではない

また、この投稿の内容は興味深いと思いますが、必ずしも実用的とは思いません。私は過去20年間、毎日ターミナルをかなりうまく使ってきましたが、これらの詳細を一切知らなくても問題ありませんでした。ただ Ctrl-CCtrl-DCtrl-ZCtrl-RCtrl-L が実際に何をするかを知っていて(おそらく Ctrl-ACtrl-ECtrl-W も)、細かいことは気にしませんでした。それでほぼ問題なく、xterm.jsを使おうとしたとき以外は困りませんでした。

しかし、これらを学ぶのは楽しかったので、皆さんにとっても興味深いかもしれません。