SmartHR Tech Blog

SmartHR 開発者ブログ

ショートカットと端末

SmartHR でプロダクトエンジニアをしている tmtms です。読み方がわからない(自分でもわからない)ので社内では tommy と名乗ってます。

2月に開催された社内LT大会第3回で「ショートカットと端末」というネタで発表したのですが、とても5分に収まる量ではなかったのでここにしたためておきます。

キーバインド

macOSのテキスト編集のショートカットとEmacsとbashのキーバインドを比較するとこんな感じです。

control macOS Emacs bash
A 行頭に移動 行頭に移動 行頭に移動
B 左に移動 左に移動 左に移動
C - prefix 中断
D 右文字削除 右文字削除 右文字削除/入力終了
E 行末に移動 行末に移動 行末に移動
F 右に移動 右に移動 右に移動
G - 中断 中断
H 左文字削除 ヘルプ 左文字削除
I - タブ タブ
J - 改行 改行
K 行末まで削除 行末まで削除 行末まで削除
L 再表示 再表示 再表示
M - 改行 改行
N 下に移動 下に移動 履歴の下に移動
O 1行挿入 1行挿入 1行挿入
P 上に移動 上に移動 履歴の上に移動
Q - 次の文字そのまま 出力再開
R - 上に検索 履歴を上に検索
S - 下に検索 出力停止
T 左右の文字入れ替え 左右の文字入れ替え 左右の文字入れ替え
U - 繰り返し 行頭まで削除
V 下ページに移動 下ページに移動 次の文字そのまま
W - 範囲削除 左の単語削除
X - prefix prefix
Y - 貼り付け 貼り付け
Z - 停止/最小化 停止

赤字のものが同じものです。だいたい同じですね。つまり macOS ユーザーは実質 Emacs ユーザーであると言っても過言ではないのです! まあ macOS も bash も Emacs のキーバインドが元になってるんで、同じなのは当然なんだけども。

端末

同じものではなく、逆に bash が Emacs と異なるものについて見てみます。

control Emacs bash
C prefix 中断
D 右文字削除 右文字削除/入力終了
H ヘルプ 左文字削除
Q 次の文字そのまま 出力再開
S 下に検索 出力停止
U 繰り返し 行頭まで削除
V 下ページに移動 次の文字そのまま
W 範囲削除 左の単語削除

これらがなぜ Emacs と違うのかというと、bash の登場前から端末での操作に使われていたキーだからです。 たとえば bash でコマンド入力中に control A を押すと行頭に移動しますが、cat コマンド入力中ではその機能は効きません。control U はどちらでも行頭までの削除として働きます。cat は control U について特別な処理をしているわけではないのですが、そのように動くのは OS の端末機能によるものです。

現代で「端末」というと端末エミュレーターのことです。macOS だと「ターミナル」とか iTerm2 とか、Windows だと TeraTerm とか PuTTY とか、Linux だと xterm とか「ターミナルエミュレーター」とかです。

端末で「A」キーを押すと 0x41 データがホストに送信され、ホストが 0x41 データを端末に送信すると端末に「A」が表示されます。端末で送受信するデータは基本的に文字です。

文字しか送受信できないのに、文字を消したりとか、画面上の任意の位置にカーソルを移動したりとか、文字の色を変えたりできているのは、どのような仕組みによるものなのでしょうか。

エスケープシーケンス

ESC (0x1B) といくつかの文字の組み合わせでひとつの機能を実現することをエスケープシーケンスといいます。

たとえば xterm の場合、

  • 右矢印キー(kcuf1) : ESC O C
  • カーソル位置移動(cup) : ESC [ row ; col H
  • 文字色変更(setaf) : ESC [ 3 color m

となります。

エスケープシーケンスは端末によって異なります。端末ごとのエスケープシーケンスは terminfo データベースに定義されています。

terminfo データベースは infocmp コマンドを使用して表示できます。 端末が xterm-256color の場合、次のような出力になります。

$ infocmp
#   Reconstructed via infocmp from file: /usr/share/terminfo/x/xterm-256color
xterm-256color|xterm with 256 colors,
    am, bce, ccc, km, mc5i, mir, msgr, npc, xenl,
    colors#0x100, cols#80, it#8, lines#24, pairs#0x10000,
    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=\r,
    csr=\E[%i%p1%d;%p2%dr, cub=\E[%p1%dD, cub1=^H,
    cud=\E[%p1%dB, cud1=\n, cuf=\E[%p1%dC, cuf1=\E[C,
    cup=\E[%i%p1%d;%p2%dH, cuu=\E[%p1%dA, cuu1=\E[A,
    cvvis=\E[?12;25h, dch=\E[%p1%dP, dch1=\E[P, dim=\E[2m,
    dl=\E[%p1%dM, dl1=\E[M, ech=\E[%p1%dX, ed=\E[J, el=\E[K,

TERM 環境変数が現在の端末の種類を示しています。 端末の機能を使いたいプログラムは TERM 環境変数の値から現在の端末の種類を得て、terminfo データベースを参照します。 使っている端末と TERM 環境変数の値が合わないと動きがおかしくなります。ssh 等でサーバーに接続したときにおかしな動きをする場合はこれが原因かもしれません。

tput コマンドでエスケープシーケンスを出力できます。シェルスクリプト等で文字に色をつけたいときとかに便利です。

$ tput setaf 1 | od -tx1c
0000000  1b  5b  33  31  6d
        033   [   3   1   m
0000005

$ tput setaf 1; echo hoge
hoge

最近の端末はだいたい256色表示できます。

$ for c in $(seq 0 255); do tput setaf $c; echo -n ■; done; echo

端末内で256色表示

OSの端末機能

たとえば cat > file コマンドを実行して文字を入力すると、cat コマンドの出力は file にリダイレクトされているはずですが、入力した文字が端末に表示されます。また、改行コードを入力するまで file には何も書かれません。 これらはOSの端末機能によるものです。

行編集

OSは端末からの入力は改行が入力されるまでプログラムに渡しません。改行が入力されるまでの間はある程度の文字編集ができます。

  • control H や BS や DEL で文字を消す
  • control W で単語を消す
  • control U で行削除
  • control D で入力を終わらせる
  • control V で次の入力をそのまま

などはできますが、矢印キーや control A,B,E,F での移動等の機能はありません。

シグナル

control キーで端末上で動いてるプログラムにシグナルを送ることもできます。これも OS の機能です。

  • control C でプログラムを終了 (SIGINT)
  • control Z で停止 (SIGTSTP)
  • control \ でコアダンプ終了 (SIGQUIT)

フロー制御

端末上で control S を押すと出力が停止されます。control Q で出力が再開されます。

  • control S で出力停止
  • control Q で再開

うっかり押してしまうと何も出力されなくなるんですが、入力はちゃんとプログラムに渡ってるのであわてていろんなキーを押さないようにしましょう。

もともとフロー制御は無手順通信で受信データの処理が追いつかなくてデータをロストしてしまわないようにするための機能だったので、今となっては無用の長物なので無効にしてしまうのもいいと思います。

stty

OSの端末機能を制御するのが stty コマンドです。stty -a で現在の設定が表示されます。

$ stty -a
speed 38400 baud; rows 24; columns 80; 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

intr から discard までは特殊な動きをするキーが設定されています。undef を指定するとその機能は無効になります。 control C で SIGINT シグナルが端末上で動作しているプロセスに送られるのは intr = ^C が設定されているからです。

たとえば intreof を無効化すれば control C でも control D でも cat コマンドを終わらなくさせることもできます。

$ stty intr undef eof undef
$ cat
^C^C^C^D^D^D

control S で出力を停止しなくするには stop を無効化します。

$ stty stop undef

control S が端末で特別扱いされなくなり bash に渡されるので、bash の本来の機能である履歴を下に検索する機能が動くようになって便利です。

入力文字をエコーバックしないようにするには -echo を指定します。

$ stty -echo

パスワードの入力とかにいいかもしれません。

$ echo -n "Password: "; stty -echo; read pw; stty echo; echo -e "\nyour password is $pw"
Password: (入力が見えない)
your password is hogehoge

行編集モード(icanon)をオフにすると1バイトずつプログラムに渡されます。

$ ruby -e 'loop{puts $stdin.read(1).upcase}'
abcd⏎  ←通常は改行するまでプログラムは入力待ちになる
A
B
C
D

$ stty -icanon; ruby -e 'loop{puts $stdin.read(1).upcase}'
aA
bB
cC
dD

端末がらみのシグナル

最後に端末絡みのシグナルをいくつか紹介します。

SIGHUP

端末が切断されたときに、その端末上で動いていたプロセスに対して送られます。 デフォルト動作は終了なので何もしてなければプロセスが終了します。 nohup コマンドを使うと SIGHUP を無視するので、端末を閉じてもプロセスは終了しません。

もうひとつの方法として disown コマンドを使ってプロセスを端末から切り離すという手もあります。 これはプログラム開始後でもできるので便利です。

$ sleep 99999 &
[1] 150539
$ jobs
[1]+  実行中               sleep 99999 &
$ disown %1
$ jobs
$ 

ところで、デーモンプログラムは SIGHUP で設定ファイルを再読み込みするような動きをするものが多いんですが、これは SIGHUP の本来の用途とは違うと思うんですが何由来なんでしょうね。

SIGTSTP と SIGCONT

SIGTSTP は control Z で停止したときに送られます。デフォルト動作は Stop なのでプログラムは停止します。 fg / bg コマンドで停止状態のプロセスが再開したときに SIGCONT が送られプロセスが再開します。

SIGTSTP を無視すると control Z で止まらなくなります。

$ bash -c 'trap "" SIGTSTP; sleep 9999'
^Z^Z^Z

SIGWINCH

端末の大きさが変更されたときに SIGWINCH がプロセスに送られます。 デフォルト動作は無視なので何も起きません。 端末サイズが変更されたことをプログラムから知ることができます。 vim など端末サイズが動作に影響するプログラムで使用されます。

$ ruby -e 'trap(:SIGWINCH){system "stty size"}; sleep'
23 80  ← 端末のサイズが変更されるたびに出力される
23 81
22 81
22 82

おわり

なんでこの内容を5分間のLTで発表しようとしたんだろう…。