⌨️

なぜ Control-/ がターミナルで使えないか調べたらターミナルの歴史を紐解くことになった

2021/10/14に公開

まえがき

普段MacでターミナルとしてiTerm2を使っていて、シェルでundoをするのに Control-/ を使っているのだけれど、別のターミナルを使った時に Control-/ でundo出来なかった。この謎を解明するため、我々調査隊はアマゾンの奥地へと向か...う必要なかったが、50年ほど時を遡る必要があった。

本編

普段「ターミナル」と呼んでいるものは正確にはterminal emulatorであり、本物の「ターミナル」をエミュレートしたものである。本物のターミナルとはディスプレイとキーボードがセットになった入出力機器[1]であり、昔はこれをコンピュータ本体にシリアル通信で繋げて入出力していた。便宜上この本物のターミナルをphysical terminalと呼ぶことにする。
多くのphysical terminalでは、キー入力は7-bitのデータに変換されシリアル通信を通してコンピュータに送信されていた。このデータの変換ルールはASCIIを採用しているphysical terminalが多かった。
普通の(control characterでない)文字はキーボードに表記されてる文字が送信されていて、0x01-0x1Aのcontrol character は Control-A - Control-Z にマッピングされていた。しかしそれ以外のcontrol characterに関してはキーマップがphysical terminalによってバラバラだった[2]
詳細な経緯はわからなかったが、このバラバラのキーマップは時間が経つにつれ統一され以下のようになったらしい。

Key Name Hex
Control-@ NUL 0x00
Control-[ ESC 0x1B
Control-\ FS 0x1C
Control-] GS 0x1D
Control-^ RS 0x1E
Control-_ US 0x1F
Control-? DEL 0x7F

そして現代の多くのterminal emulatorは上記のキーマップに従って実装されているようだ[3]
で、この表を見ると Control-/ が入っていない。つまり「なぜ Control-/ がターミナルで使えないか」の答えは、 Control-/ の変換先のデータがないため Control-/ を押してもterminal emulatorで開いているアプリケーションにデータが送信されず、undoのショートカットとして使えない、ということだった。

また、ここでもう一つ疑問になるのがなぜiTerm2では Control-/ が使えるのか?ということだけれど、このキーマップがばらばらだった時代、一部のphysical terminalでは 0x1F の送信に Control-/ がマッピングされていたため、iTerm2はそれを尊重し Control-/ が使えるようにしているのだと思われる[4]。 Control-/ が使える代表的なphysical terminalとしてVT-100がある。

参考

おまけ

これらを調べてる間に知った、ためになった・面白かった知識をせっかくなのでまとめる。

キーボード入力がアプリケーションに届くまでの仕組み

physical terminalの場合。
前述の通りキーボードの入力をphysical terminalがASCIIに変換してシリアル通信経由でコンピュータ本体に送信する。コンピュータ本体側では、そのデータをアプリケーションに渡す前にline disciplineと言うコンポーネントがあり、これがcontrol characterをハンドルする。普通の文字はそのままアプリケーションに届く。

Linuxのterminal emulatorの場合。
kernelが上記のシリアル通信とline disciplineの役割を担う仮想デバイスptyを提供している。ptyにはmasterとslaveがあり、masterがterminal emulatorからデータを受け取り、slaveがアプリケーションのstdin, stdout, stderrにセットされる。これらを経由し、キーボード入力→terminal emulatorがデータに変換→pty master→line discipline→pty slave(=stdin, stdout, stderr)→アプリケーション、と言うの流れになる。control characterの場合はptyの中のline disciplineが適切な処理を行う。

terminal emulatorで "delete" ボタンで文字が削除されたり、 Control-C でプログラムを終了出来る(=SIGINTをプログラムに送信する)のはline disciplineのおかげ。

line disciplineが何をやっているかは stty -a コマンドで確認できる。

$ stty -a
speed 38400 baud; rows 55; columns 156; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc

terminal emulatorで(ほとんどの)Controlキーを使ったショートカットがShiftを押しても同じ挙動になる理由

physical terminalがそういう挙動だったから。Controlキーを押すとキー入力のbit 6, bit 7を0にする実装が多かったらしい。例えば A は 0x41 で、2進法だと 0b1000001 で、bit 6, bit 7を0にすると 0b0000001 になり、これがcontrol characterの 0x01(SOH) になる。 a は 0x61 で 0b1100001 なので、bit 6, bit 7を0にすると同じ 0b0000001 になる。

またControlキーを押しながらキー入力をした時に起きることは、上記のように対応するcontrol characterを送信することであり、そもそもASCIIにおけるcontrol characterは33個しかない[5]ので、Shiftキーを押しながらで別のコードを送信したくなったところで(基本的に)別のキー入力と同じcontrol characterを送ることしか出来ない。

ASCIIは7-bitなので128文字しか定義出来ないが、その中で小文字アルファベットを諦めてcontrol characterを増やす案もあったらしい。control characterを増やした世界線であれば、きっとShiftを押したら別のコードを送信するようになっていただろうと思う。

ttyとは何か

TeleTypewriterの略でtty。

teletypewriterはphysical terminalの一種。本編で説明したディスプレイ付きのphysical terminalなどよりも昔の機械で、ディスプレイを持たず代わりにプリンタが付属している。対してディスプレイを持つphysical terminalはVideo Display Unit(VDU)と呼ばれる。

これが由来でterminal emulatorとアプリケーションを接続する間の仮想デバイスがttyを呼ばれるようになった。 tty コマンドでその仮想デバイスを確認できる。

$ tty
/dev/pts/0

また、前述の通りphysical terminalはシリアル通信でコンピュータと接続していたため、その名残でLinuxでシリアルポート形式のデバイスの名前全般にttyが用いられるようになった[6]

環境変数TERMについて

前述の通り、昔はいろんなphysical terminalが存在し、仕様が統一されておらずphysical terminalごとにいろいろ違った。しかしコンピュータ側で動くプログラムは出来るだけ多くのphysical terminalでも動いてほしい。これを解決するために作られたものの1つがTERMと言う環境変数で、これを参照することによってphysical terminalごとの適切な挙動をするようなプログラムを作ることができた。

例えばTERM=xtermでvimでjsonファイルを開くと色がつく

$ export TERM=xterm
$ vim.basic hoge.json

が、TERM=vt100だと色がつかない

$ export TERM=vt100
$ vim.basic hoge.json

vimが$TERMの値を確認し、xtermには色表示機能があるため色付きデータを出力し、vt100には色表示機能がないため色なしデータを出力しているためこうなる(はず)。

また、vimがすべてのterminal情報を持っていて判断しているわけではなく、terminfoと言うデータベースを参照し判断している(はず)。この情報は infocmp コマンドで確認できる。これを見るとxtermには colors#8 とあるが、vt100には同様の記述がないことが分かる。

$ infocmp xterm
#	Reconstructed via infocmp from file: /lib/terminfo/x/xterm
xterm|xterm-debian|X11 terminal emulator,
	am, bce, km, mc5i, mir, msgr, npc, xenl,
	colors#8, cols#80, it#8, lines#24, pairs#64,
	acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
	bel=^G, blink=\E[5m, bold=\E[1m, cbt=\E[Z, civis=\E[?25l,
	clear=\E[H\E[2J, cnorm=\E[?12l\E[?25h, cr=^M,
(略)

$ infocmp vt100
#	Reconstructed via infocmp from file: /lib/terminfo/v/vt100
vt100|vt100-am|dec vt100 (w/advanced video),
	am, mc5i, msgr, xenl, xon,
	cols#80, it#8, lines#24, vt#3,
	acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
	bel=^G, blink=\E[5m$<2>, bold=\E[1m$<2>,
	clear=\E[H\E[J$<50>, cr=^M, csr=\E[%i%p1%d;%p2%dr,
(略)

データベースにあるterminal情報リストは toe -a コマンドで確認できる。

$ toe -a
rxvt-unicode	rxvt-unicode terminal (X Window System)
rxvt-basic	rxvt terminal base (X Window System)
rxvt      	rxvt terminal emulator (X Window System)
linux     	linux console
wsvt25m   	NetBSD wscons in 25 line DEC VT220 mode with Meta
wsvt25    	NetBSD wscons in 25 line DEC VT220 mode
cygwin    	ansi emulation for Cygwin
(以下略)
  • 詳細は toe (1), terminfo (5), infocmp (1) 参照。

キー入力するとたまに出力される ^[[C のような謎の文字列は何か

例えば、 tail -f してる時に十字キーの → を入力するとこうなる

$ tail -f hoge.txt
^[[C

これは何か?という話。これを探るにはやはりphysical terminalがどう使われていたかを見る必要がある。
前述したように昔はphysical terminalからcontrol characterを送信してコンピュータを制御していた。しかし、そのうちASCIIのcontrol characterの数では足りなくなってきた。そこで考え出されたのがescape sequenceと呼ばれるもので、ASCIIの ESC(0x1B) を直接コンピュータ制御にマップするのではなく、それに続いて入力されるcharacter sequenceをコンピュータ制御にマップすることによって、より多くのコンピュータ制御を行うというもの。
上記の ^[[C もescape sequenceの1つで、十字キーの → を押すことで ^[ (0x1B), [ (0x5B), C (0x43) という3 charactersが送信されている。通常terminal emulatorでシェルを開いている時に → を入力すると、このデータをterminal emulatorが送信し、シェルが受け取り、解釈して[7]カーソルが右に動く、と言う仕組みになっている。
そしてこれはシェルで実際に Escape, [, Shift-C の順番で手入力することで確認することが出来て、手動で入力してもカーソルが右に動く。

脚注
  1. 正確には初期のターミナルはディスプレイではなくプリンタだった ↩︎

  2. "The interpretation of the control key with the space, graphics character, and digit keys (ASCII codes 32 to 63) vary between systems. Some will produce the same character code as if the control key were not held down. Other systems translate these keys into control characters when the control key is held down." https://en.wikipedia.org/wiki/Control_character#How_control_characters_map_to_keyboards ↩︎

  3. Control-@ の NUL(0x00) だけは多くのphysical terminalで Control-Space にもマップされており、現代のterminal emulatorでも Control-Space 入力できるものが多い ↩︎

  4. Control-_ と同じ 0x1F が送信される https://github.com/gnachman/iTerm2/blob/89d4209c168fc43fc140f913f7093f70c4ceb0ce/sources/iTermStandardKeyMapper.m#L106-L130 ↩︎

  5. どこまでをcontrol characterとするかは諸説あるっぽいが ↩︎

  6. "tty デバイスという名前は、非常に古いテレタイプ(teletypewriter)の省略形に由来しています。 そして、元々は、Unix マシンに接続する物理ターミナル、または仮想ターミナルだけを指していました。時が経ち、ターミナルの接続にシリアルポートを使用していたという理由から、この名前が シリアルポート形式のデバイス全般を指すようになりました。" O'reilly LINUXデバイスドライバ第3版 p557 18章より ↩︎

  7. 少なくとも現代のterminal emulatorにおいてはカーソル移動を扱うのはline disciplineではなくbashなどのアプリケーション。おそらく昔のコンピュータはline disciplineがハンドルしていた。 ↩︎

Discussion