Linux/制御シーケンス

制御シーケンスを投げたり受けたりする(2019-06-06)


Androidで Sshクライアントを作った 過程で、
制御シーケンスを投げたり実装したりしました。
そのあたりを少しメモしておこうと思います。



概要

ふわっと説明します。
先に、具体例を上げておくと

 printf '\033[31mTEST\033[m\n'

こうすると、TESTという「赤い文字」が出力されます。
033というのがESCを表す文字コードで、
文字コード指定するために数字をエスケープ(\)しています。
"man ascii"コマンドがある環境の方は文字コード一覧が見れますが、
033だと8進数。これを16進数で表すと0x1b、10進数なら27番を指定するとESCです。*1

このESCで始まる特殊なコマンド(\033[31m)が制御シーケンスで、
Webで xterm control sequense とか ansi control codes とかで検索すると出てきます。
手元のUbuntuだと "man console_codes" でマニュアルが見れました。
これらを日本語でWeb検索すると、大抵はTeraTermのヘルプマニュアルに行き着くと思います。

Unix/Linuxで使われる制御コードの大本(おおもと)になるのが、
ECMA48やら、VT100やらという規格らしいのです(=ふわっとした理解)。
なので、コマンドの概要が知りたい場合はTeraTermヘルプの「制御シーケンス」の項が、
コマンドの詳細な説明が必要な際は
ECMA48関連のサイトでコマンド一覧をダウンロードしたり、
VT100関連のサイトからそのコマンド名で検索するのが早いと思います。*2

TeraTermはじめ、端末エミュレータで設定する
端末タイプ(環境変数TERM)というのが、どの制御シーケンスに対応するのかという話にも関連していて、
ざっくり言えば「vt100 + その他制御シーケンス = xterm」です。*3

むずかしいはなしはさておき、ESCコマンドを使うことで、
文字の色を変えたり、カーソル位置を変えたり、
ウィンドウのサイズを変えたり、色々できるようになります。

つまり、遊べます。 [smile]

制御コードの例

変数のTERM が xtermなんとかになっていれば
(envコマンドで確認して、sh だと "export TERM=xterm" 、cshだと "setenv TERM xterm" で設定できます)
私の使っているLinux系のOSだと ls やら top やらで実行ファイルやディレクトに色がついたり、*4
見出し部分が白黒反転表示されるようになりました。

各コマンドで、カラー対応のものはTERMを見ながら色関連の制御シーケンスを使うかどうか判断しているのだと思います。
例えばこれがTERM=vt100だと、色数が減ったりモノクロになったりする可能性が高いです。
でもモノクロの方が(情報量が減るので)処理は軽くなると思います、たぶん。

この表示、
「ls -la > ls.txt」 みたいにリダイレクションしてファイルに落とした後に表示しても、
色情報は抜け落ちて、テキストだけになると思います。
が、
これを script コマンドや、TeraTermだと「バイナリ」オプション付きで、
とにかく、出力を全てナマのまま拾うと、制御シーケンスもぜんぶ載ってきます。ESCは文字化け(印字できない)かもしれませんが。
ちなみにバックスペースボタンを押した時も(文字削除ではなく)バックスペースとしてログに載りますね。


では最初に戻って、文字の色を変える制御シーケンスの例です。

 printf '\033[31mTEST\033[m\n'
  • '\033'が制御シーケンス開始の合図です
  • '[31m'が命令部分です
    • '[ 番号 m' というのが色を指定する命令で(SGR)
      31番は「文字を赤に」です。たとえば41番だと「背景を赤に」になります。
  • 最後の'\033[m' も同様の命令で、
    '[ 番号 m' の数字なしは '[ 0 m' と解釈されます。
    つまり「文字属性をリセット」です。


なので、'\033[m' を打ち忘れると、文字が真っ赤なままになってしまいます。 [worried]
落ち着いて、「printf '\033[m'」して下さい。

使ってみよう!

色々あるので、興味が有る制御シーケンスを試しに投げてみるのが良いと思いますが、 とりあえず
(以下、見易さのためにスペース入れていますが、実際はスペースなしです)

  • '\033[ 番号 m' で表示属性を指定する(SGR:Select Graphic Rendition)
  • '\033[ 桁数 D' で左に移動(CUB: Cursor backward)
  • '\033[K' でカーソル位置から行末までを削除(EL: Erase Line)

この辺を使うことで、文字の色を変えたりしつつ、
同じ行の文章を時間経過で更新、みたいなことができます。

以下、毎秒表示の変わるshellスクリプトの例です。
適当なファイルに書き込んで、chmod 744 とかして、実行しみて下さい。

 #!/bin/sh
 
 # シェルを中断(2=SIGINT、Ctrl+Cとか)した時に文字属性をリセットする
 # 必要に応じて他のシグナルもtrapしましょう
 trap  "printf '\033[m'; exit" 2
 
 printf '==' # 最初の2文字だけ、先に出力
 
 while :; do # 以下、無限ループ。Ctrl+Cで中断します。
   SEC_TEXT=`date +%S`
 
   # 3を含むか、3の倍数の時にアオになります
   AO_FLAG=0
   echo $SEC_TEXT | grep "3" > /dev/null
   if [ $? -eq 0 ]; then AO_FLAG=1; fi
   AMARI=`expr $SEC_TEXT '%' 3`
   if [ $AMARI -eq 0 -a $SEC_TEXT != "00" ]; then AO_FLAG=1; fi
 
   if [ $AO_FLAG -gt 0 ]; then printf '\033[36m'; fi  # アオに(blueは見づらいのでcyan)
   printf '%s==' $SEC_TEXT
   printf '\033[m'  # 文字属性をリセット
 
   sleep 1
   printf '\033[4D'  # 4文字もどる
   printf '\033[K'   # 行末まで消す
 done

これ、書いて動かしてみた後に気がついたんですが(trapの件)
文字の色やカーソル位置を変える場合って、
中断した時に元に戻す処理が必要かもしれないですね。

以下、もっと複雑な感じの例。
使う制御シーケンスを増やしました。
罫線の記号が、色を変えながらくるくる回ります。

 #!/bin/sh
 
 # シェルを中断(2=SIGINT、Ctrl+Cとか)した時に
 #   文字属性をリセット  [m
 #   文字のマッピングをリセット (B
 #   カーソルを表示 [?25h
 trap  "printf '\033[m\033(B\033[?25h'; exit" 2
 
 # 端末がカラーオプションに対応している前提で、
 # カラーテーブルの16から231番をローテーションする
 COLOR_FROM16=16
 COLOR_TO231=231
 COLOR_NOW=$COLOR_FROM16
 
 printf '==X=='
 printf '\033[2D'    # 2文字もどる
 printf '\033[?25l'  # カーソルを非表示にする
 
 while :; do
   SEC_TEXT=`date +%S`
 
   # 毎秒、4種の文字をローテーションします
   SHOW_TEXT=""
   if   [ `expr $SEC_TEXT '%' 4` -eq 1 ]; then SHOW_TEXT="t";
   elif [ `expr $SEC_TEXT '%' 4` -eq 2 ]; then SHOW_TEXT="w";
   elif [ `expr $SEC_TEXT '%' 4` -eq 3 ]; then SHOW_TEXT="u";
   else SHOW_TEXT="v"
   fi
 
   COLOR_NOW=`expr $COLOR_NOW '+' 1`
   if [ $COLOR_NOW -gt $COLOR_TO231 ]; then COLOR_NOW=$COLOR_FROM16; fi
 
   printf '\033[38:5:%dm' "$COLOR_NOW" # 文字属性をカラーテーブルで指定する
   printf '\033(0'                  # 文字(SHOW_TEXT=t,w,u,v)をグラフィックマッピングへ切替える
   printf '\033[D'                  # 1文字分カーソルを左へ
   printf '%s' $SHOW_TEXT           # 文字、というか罫線を書く
   printf '\033(B'                  # 文字を戻す
   printf '\033[m'                  # 文字属性をリセット
 
   # スリープしないほうが面白いけど、リモート接続だと通信量が跳ね上がるので...
   # ローカルでやる場合は0.2秒くらい、リモート接続なら1秒くらいでどうぞ
   sleep 1 
   # uleep 200000 # もしusleepがあれば 0.2 秒の例
   # perl -e "select(undef,undef,undef,0.2);" # 同じくperlなら
 done

ここまでやらないにしても、
カーソル移動して、同じ行で上書きするだけでも、少し表示が楽しくなります。
そもそも前述の、キーボードのバックスペースボタンを押した時の動作も
scriptコマンドとかで見ると「左に戻る、スペースで上書き、左に戻る」
っていう3段階の動作をしているんですよね。
それとやっていることは大して変わりません。

以上です。
これでCUIだけどCUIっぽくない感じの画面表示に挑戦してみましょう!

端末エミュレータ?

...ここからは特殊な事情、趣味を抱える方に向けて。

制御シーケンスを投げるときではなく、受け取った時にどうするか
つまり、端末エミュレータ的なものを実装する時に考慮しなければならない話です。

何から調べて、どう実装すれば良いのやら、という方へ...
とりあえず、とっかかりとしては以下の3案はいかがでしょうか

  • まずは既存のライブラリを探す
  • TeraTermのソースコードから、vtterm.c を読む
  • 制御シーケンスには触れないようにする

ライブラリは、自分の使うプログラミング言語に合わせて、
いわゆるvt100エミュレータ的なやつを探せば、色々と見つかります。
それらが制御シーケンス部分をいい感じに処理して、標準入出力との橋渡しをしてくれるはずです。
(キーボードの入力をサーバに渡せる形にしたり、
サーバからの応答を画面に出力できる段階まで翻訳してくれたり)
vt100?...俺はvt410が無いと嫌だ!という方は、
エミュレータなど使わず、VT410(実機)を買えばいいと思います。*5
いや、たぶん多くの人はvt100で十分なはずですから...

自力で制御シーケンスを読んだり作ったりしたい場合は、
vtterm.c がとても参考になります。
最終的には ecma-international.org や vt100.net で各コマンドの説明(英語)を見ることでようやく、具体的な仕様が分かってくるわけですが、
それ以前に、実装例と言うか、雰囲気とか、心意気とか、そういうのが無いと、辛いので。*6
...辛いので。*7

「制御シーケンスには触れないように」というのは、
前述のscriptコマンドのようにリアルタイムに出力を拾うのではなく、
リダイレクションなどで一旦ファイルに落としたものを読んで、
(パイプやら、tail -f やら tee -a やら、間に一枚コマンドをはさんで)
制御シーケンスを処理したあとのテキストを、ややリアルタイム気味に読めば良いのでは?
というお話です。
用途によっては、あまり対話式処理にこだわらないほうが良い気がします。*8


ライブラリにせよ自力にせよ、やってみるなら、
実装の対象とする制御コードは、まずは自分が使うやつだけで良いと思います。
最初はカーソル移動と、行(K)と画面(J)の削除があれば十分でしょう。
それよりも先に、バックスペース、CR、LF、Auto-Wrap*9の扱いで戸惑うのではないでしょうか。
それをふまえて、私が順に対応していったのは...

  1. 自分が使いそうな、簡単な制御シーケンス
  2. 自分が使うコマンドに出てくるやつ、viで飛ぶ制御シーケンス、とか
  3. vt100ぜんぶ vt100.net でVT100の範囲とされているもの(VT200とか410とかではなく)
  4. Linux の console_codes
  5. xtermぜんぶ xfree86.org/current/ctlseqs.html
  6. 現在のxtermぜんぶ? invisible-island.net/xterm/ctlseqs/ctlseqs.html

それにともなって、動作確認に使うコマンドも徐々に複雑にすれば良いかと
(くじょうはうけつけません。こころがおれます)

  1. ls
    実装できていないと、表示がズレたり色が反映されなかったりします
  2. top コマンド
    行削除 K やカーソル移動 H、SI/SOの扱いが正しく実装されていないと
    みるみるうちに、混沌としてきます
  3. more/less コマンド
    スクロールできるようにしましょう。
    バックインデックス?なにそれ?
  4. viなどのエディタ
    挿入、削除を繰り返すだけで表示と心が崩れていきます
  5. w3mなど、テキストブラウザ
    マウスイベント?なにそれ?
    ...全角文字と特殊文字で、どんどん位置がズレるんだが!?
  6. vttest
    タスケテ!

最初は ls と tail でログが見れれば十分だったんです...
printf で TEST をアカくしていたころが、なつかしい...
...おれは、もう...ここまでだ...あとは...頼む...*10


*1 echo とか printf とかで直に渡せるのは8進数っぽいですね。16進数指定したい場合はそれぞれのshellの拡張機能を使うか、perlとかのスクリプト言語を使うとやり易いです
*2 例えば、画面端から更に上にカーソル移動が発生した場合、カーソルが止まるのか、画面がスクロールするのかという詳細な説明は本家のサイトを探すことになります。
*3 invisible-island.netで xtermの制御シーケンス一覧 を見れるのですが(英語)、ここではコマンドの横に"VT320"とかVT端末のバージョンも書いてあるので参考になります
*4 viコマンド等のエディターなんかは制御シーケンス山盛りですが、最初にこれをログで見てもハードルが高すぎると思うので...。topよりも、htopなら色の違いがはっきり出ると思います
*5 でも冗談でもなく、接続先と同じOSを自分のノートPCに入れてsshするのが、一番安全確実なのではないでしょうか?
*6 本格的に追う場合は、あわせてctagsコマンド等のコードアシスト機能も使ってみましょう
*7 自力、車輪の再発明はほんとうに辛いのでオススメできません...まぁ、楽しかったですけどね。
*8 何かコマンドを実行して即ログアウト、の場合は制御シーケンスは要りません。例えば私も使っているJSchの場合はこちらで見れます(Java)
*9 CRはカーソルを左へ、LFはカーソルを次の行へ、二つ合わさってはじめて一般的にイメージする改行(次の行の左端へ)になります。AutoWrapは画面右端で自動的に改行する/しないの話で、これも自力でそう作る必要があります
*10 私の場合は...色々ありますが、ハイライトマウスは怖いから実装していないのと、実装したけど諦めているのが文字コード関連です。

  最終更新のRSS