エスケープシーケンス
/mrsekut-book-4297127474/055
ts
ターミナルエミュレータは本質的に「バイトストリームのインタプリタ」である。シェルやアプリケーションが stdout に書き出すバイト列には、表示すべき文字だけでなく、カーソル移動・色変更・画面消去などの 制御命令 が混在している。これらの制御命令をインバンドで表現する仕組みが エスケープシーケンス である。
歴史的背景
VT100 (1978年、DEC社): ANSIエスケープシーケンスを実装した最初の広く普及した端末
ECMA-48 / ANSI X3.64: エスケープシーケンスの標準仕様
xterm: X Window System上のターミナルエミュレータとして事実上の拡張標準を定義
バイトストリーム処理の基本フロー
code:_
アプリケーション (stdout)
│
▼
バイトストリーム: "Hello\x1b[31mWorld\x1b[0m\n"
│
▼
VTパーサー
│
├─ テキスト "Hello" → セルに書き込み
├─ ESC [ 31 m → 前景色を赤に変更
├─ テキスト "World" → 赤色でセルに書き込み
├─ ESC [ 0 m → 属性リセット
└─ LF → カーソルを次の行へ
上記の \x1b は ESC 文字 (0x1B) であり、これがエスケープシーケンスの開始を示す。パーサーはこのバイトを受け取った瞬間に「エスケープ状態」に遷移し、後続のバイトを制御命令として解釈する。
2. 主要なシーケンスの分類と具体例
2.1 C0 制御文字 (0x00–0x1F)
単一バイトで制御を行う文字群。エスケープシーケンスとは別に、常にインターセプトする必要がある。
table:_
バイト 名前 動作
0x00 NUL 無視
0x07 BEL ベルを鳴らす(OSCの終端としても使用)
0x08 BS カーソルを1つ左に移動
0x09 HT 次のタブストップに移動
0x0A LF 改行(カーソルを1行下へ。設定によってはCRも同時に行う)
0x0B VT LFと同じ扱い
0x0C FF LFと同じ扱い
0x0D CR カーソルを行頭に移動
0x1B ESC エスケープシーケンスの開始
2.2 C1制御文字 (0x80–0x9F)
8ビットのC1制御文字は、2バイトのESCシーケンスと等価である。現代のUTF-8環境ではC1範囲のバイトがUTF-8のリードバイトと衝突するため、7ビット形式 (ESC + 第2バイト) が一般的に使われる。
table:_
C1 (8bit) 7bit形式 名前 説明
0x84 ESC D IND Index (カーソルを1行下へ)
0x85 ESC E NEL Next Line (CR + LF)
0x88 ESC H HTS Horizontal Tab Set
0x8D ESC M RI Reverse Index (カーソルを1行上へ)
0x90 ESC P DCS Device Control String 開始
0x9B ESC [ CSI Control Sequence Introducer
0x9C ESC \ ST String Terminator
0x9D ESC ] OSC Operating System Command
実装上のポイント: UTF-8対応のターミナルでは、0x80–0x9F の範囲をC1制御文字として解釈するかどうかが問題になる。xterm はUTF-8モードではC1を無視する。自作実装でもUTF-8モード時は7ビット形式のみ対応すれば十分。
2.3 CSI
2.4 OSC
2.5 DCS
2.6 SS2/SS3 (Single Shift)
SS2 (0x8E / ESC N) とSS3 (0x8F / ESC O) は、次の1文字だけ別の文字集合から取得することを指示する。
code:_
SS3はファンクションキーのレポートに頻出:
F1: \x1bOP (ESC O P)
F2: \x1bOQ
F3: \x1bOR
F4: \x1bOS
アプリケーションモード時のカーソルキー:
↑: \x1bOA (通常モードでは \x1b[A)
↓: \x1bOB
→: \x1bOC
←: \x1bOD
アプリケーションモード時のテンキー:
Enter: \x1bOM
0: \x1bOp
1: \x1bOq
...
3. SGR
Alternate Screen Buffer
5. VTパーサー
6. 対応すべきシーケンスの優先順位
Tier 1: 最低限必要(シェルが動作するために必須)
これらがないとbashやzshがまともに表示できない。
code:_
C0制御文字:
BS (0x08), HT (0x09), LF (0x0A), CR (0x0D), ESC (0x1B)
CSI カーソル操作:
CUU (A), CUD (B), CUF (C), CUB (D) — カーソル移動
CUP (H) — カーソル位置設定
ED (J) — 画面消去
EL (K) — 行消去
SGR (m) — テキスト属性 (最低限 0, 1, 30-37, 40-47, 39, 49)
CSI モード:
DECTCEM (?25h / ?25l) — カーソル表示/非表示
DECAWM (?7h / ?7l) — 自動折り返し
ESCシーケンス:
RI (ESC M) — Reverse Index
デバイス応答:
DA (CSI c) — デバイス属性応答
DSR (CSI 6 n) — カーソル位置報告
OSC:
OSC 0/1/2 — タイトル設定
Tier 2: 主要アプリケーション対応(vim, tmux, htop等)
code:_
代替スクリーン:
?1049h / ?1049l — 代替スクリーンバッファ
スクロール:
DECSTBM (CSI r) — スクロール領域設定
SU (CSI S) / SD (CSI T) — スクロールアップ/ダウン
IND (ESC D) — Index
SGR 拡張:
256色 (38;5;n / 48;5;n)
True Color (38;2;r;g;b / 48;2;r;g;b)
全装飾 (2-9, 21-29)
カーソルキーモード:
DECCKM (?1h / ?1l) — アプリケーションカーソルキー
文字操作:
ICH (CSI @) — 文字挿入
DCH (CSI P) — 文字削除
ECH (CSI X) — 文字消去
IL (CSI L) — 行挿入
DL (CSI M) — 行削除
カーソル保存/復元:
DECSC (ESC 7) / DECRC (ESC 8)
CSI s / CSI u
Bracketed Paste:
?2004h / ?2004l
Tier 3: 高度な機能(後回しにできる)
code:_
OSC 8 — ハイパーリンク
OSC 52 — クリップボード操作
OSC 133 — シェル統合
OSC 4/10/11/12 — カラーパレット操作
DCS:
Sixel Graphics
DECRQSS
マウスレポート:
?1000h (ボタンイベント)
?1002h (ボタン+ドラッグ)
?1003h (全マウスイベント)
?1006h (SGRマウスモード — 推奨)
フォーカスイベント:
?1004h
文字集合:
G0/G1/G2/G3文字集合の切り替え
DEC Special Character and Line Drawing Set
ウィンドウ操作:
CSI t (各種ウィンドウ操作)
Kitty拡張:
Kitty Keyboard Protocol
Kitty Image Protocol
Tier 4: 完全互換を目指す場合
code:_
Sixelグラフィックス
iTerm2 Image Protocol
DEC特殊モード群 (DECCOLM, DECOM, DECINLM...)
プリンタ制御
SOS/PM/APCシーケンス
VT52互換モード
7. 既存パーサーライブラリの比較
7.1 [vte
7.2 libvterm
7.3 xterm.js 内蔵パーサー
ブラウザ上で動作する TypeScript 製のターミナルエミュレータ。VS Code の統合ターミナルとしても使われている。
リポジトリ: https://github.com/xtermjs/xterm.js
特徴
TypeScript で実装されたフルスタックのターミナルエミュレータ
パーサー部分 (EscapeSequenceParser) は独立したクラスとして分離されている
WebGL/Canvas レンダラーを内蔵
アドオン機構 (ITerminalAddon) で機能拡張が可能
パーサーのAPI概要
code:typescript
import { Terminal } from 'xterm';
// ターミナルの作成
const terminal = new Terminal({
cols: 80,
rows: 24,
scrollback: 1000,
});
// DOMにマウント
terminal.open(document.getElementById('terminal')!);
// バイト列を入力
terminal.write('\x1b[31mHello\x1b[0m World');
// カスタムCSIハンドラの登録
// 例: 独自のCSIシーケンス \x1b[?9999h を処理
terminal.parser.registerCsiHandler(
{ prefix: '?', final: 'h' },
(params) => {
if (params.params0 === 9999) {
console.log('Custom mode activated!');
return true; // 処理済み
}
return false; // デフォルトハンドラに委譲
}
);
// カスタムOSCハンドラの登録
terminal.parser.registerOscHandler(1337, (data) => {
// iTerm2スタイルの独自OSCを処理
console.log('Custom OSC:', data);
return true;
});
code:typescript
// xterm.js のパーサー部分だけを使いたい場合
// (内部APIのため直接アクセスは非推奨だが、構造理解のために記載)
// EscapeSequenceParser のコア構造:
//
// 状態遷移テーブル: Uint8Array ベースのルックアップテーブル
// 各状態 × 各入力バイト → (アクション, 次の状態)
//
// アクション:
// PRINT - 通常文字出力
// EXECUTE - C0制御文字実行
// CSI_DISPATCH - CSIシーケンス完了
// OSC_START - OSC開始
// OSC_PUT - OSCデータ蓄積
// OSC_END - OSC終了
// ...
// xterm.js v5+ ではパーサーが WebAssembly で高速化されている
長所
ブラウザ環境でそのまま使える唯一の選択肢
VS Code で使われており、非常に幅広いアプリケーションとの互換性が検証済み
アドオン機構でWebGL描画、検索、画像表示等を追加可能
TypeScript の型安全性
短所
ブラウザ/Node.js 環境前提(ネイティブアプリケーションでは使いにくい)
パーサー単体の分離が困難(ターミナル全体と密結合)
DOMレンダリング関連のオーバーヘッド
メモリ使用量がネイティブ実装より大きい
ライブラリ比較表
table:_
特性 vte (Rust) libvterm (C) xterm.js (TS)
言語 Rust C TypeScript
提供範囲 パーサーのみ パーサー+スクリーン フルスタック
ヒープ割り当て ゼロ (パーサー) あり あり
UTF-8対応 あり あり あり
Sixel対応 なし なし アドオン
スクリーンバッファ なし (自前実装) あり あり
リサイズ/リフロー なし あり あり
使われている場所 Alacritty, Wezterm Neovim, Vim VS Code, Hyper
ライセンス Apache-2.0/MIT MIT MIT
学習コスト 低 中 中〜高
8. テスト方法
8.1 vttest
vttest は VT100/VT220 互換性テストプログラムで、ターミナルの動作を体系的にテストできる。
code:bash
# インストール
# macOS:
brew install vttest
# Ubuntu/Debian:
sudo apt install vttest
# ソースからビルド:
git clone https://github.com/ThomasDickey/vttest
cd vttest && ./configure && make
code:_
vttest を起動すると、メニュー形式でテスト項目を選べる:
1. Test of cursor movements
2. Test of screen features
3. Test of character sets
4. Test of double-sized characters
5. Test of keyboard
6. Test of terminal reports
7. Test of VT52 mode
8. Test of VT102 features (Insert/Delete Char/Line)
9. Test of known bugs
10. Test of reset and self-test
11. Test non-VT100 (strstrstr) terminals
12. Test of color features
8.4 既存ターミナルとの出力比較
信頼できるターミナルの出力と自作実装の出力を比較する方法:
code:bash
# 1. script コマンドで出力をキャプチャ
script -q /tmp/terminal_output.log
# ... 操作を行う ...
exit
# 2. キャプチャしたバイト列を16進ダンプ
xxd /tmp/terminal_output.log | head -50
# 3. 特定のアプリケーションの出力をキャプチャ
script -q /tmp/vim_output.log -c 'vim -c "q"'
script -q /tmp/htop_output.log -c 'htop -d 1' # 短時間だけ実行
# 4. expect/tmux を使った自動テスト
# tmux の capture-pane を使って、期待される画面内容と比較
tmux new-session -d -s test -x 80 -y 24
tmux send-keys -t test 'printf "\x1b[31mRed\x1b[0m"' Enter
sleep 0.5
tmux capture-pane -t test -p > /tmp/actual_output.txt
8.5 ファズテスト
パーサーの堅牢性をテストするため、ランダムなバイト列を入力してクラッシュしないことを確認する:
code:rust
// cargo-fuzz を使ったファズテスト
// fuzz/fuzz_targets/parser_fuzz.rs
#!no_main
use libfuzzer_sys::fuzz_target;
use vte::{Parser, Perform};
struct NullHandler;
impl Perform for NullHandler {
fn print(&mut self, _c: char) {}
fn execute(&mut self, _byte: u8) {}
fn csi_dispatch(&mut self, _params: &vte::Params, _intermediates: &u8,
_ignore: bool, _action: char) {}
fn osc_dispatch(&mut self, _params: &[&u8], _bell_terminated: bool) {}
fn esc_dispatch(&mut self, _intermediates: &u8, _ignore: bool, _byte: u8) {}
fn hook(&mut self, _params: &vte::Params, _intermediates: &u8,
_ignore: bool, _action: char) {}
fn unhook(&mut self) {}
fn put(&mut self, _byte: u8) {}
}
fuzz_target!(|data: &u8| {
let mut parser = Parser::new();
let mut handler = NullHandler;
for &byte in data {
parser.advance(&mut handler, byte);
}
});
code:bash
# ファズテストの実行
cargo install cargo-fuzz
cargo fuzz run parser_fuzz -- -max_len=4096
8.6 参考リソース
VT100.net: https://vt100.net/ — DEC端末のオリジナルマニュアル群
Paul Williams の状態遷移図: https://vt100.net/emu/dec_ansi_parser — パーサー実装の基盤
xterm ctlseqs: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html — xterm の全シーケンスリファレンス(事実上の標準仕様書)
ECMA-48: https://ecma-international.org/publications-and-standards/standards/ecma-48/ — 公式標準仕様
Terminal Guide: https://terminalguide.namepad.de/ — 各ターミナルの対応状況を比較
付録: バイトレベルでの解析例
以下は、実際のバイトストリームをパーサーがどう処理するかのトレース例:
code:_
入力: \x1b[38;2;255;128;0mHello\x1b[0m\n
バイト列 (16進):
1B 5B 33 38 3B 32 3B 32 35 35 3B 31 32 38 3B 30 6D
48 65 6C 6C 6F
1B 5B 30 6D
0A
パーサーのトレース:
byte=0x1B state: Ground → Escape action: none
byte=0x5B state: Escape → CsiEntry action: none
byte=0x33 state: CsiEntry → CsiParam action: param (3)
byte=0x38 state: CsiParam → CsiParam action: param (38)
byte=0x3B state: CsiParam → CsiParam action: param_separator → params=38
byte=0x32 state: CsiParam → CsiParam action: param (2)
byte=0x3B state: CsiParam → CsiParam action: param_separator → params=38,2
byte=0x32 state: CsiParam → CsiParam action: param (2)
byte=0x35 state: CsiParam → CsiParam action: param (25)
byte=0x35 state: CsiParam → CsiParam action: param (255)
byte=0x3B state: CsiParam → CsiParam action: param_separator → params=38,2,255
byte=0x31 state: CsiParam → CsiParam action: param (1)
byte=0x32 state: CsiParam → CsiParam action: param (12)
byte=0x38 state: CsiParam → CsiParam action: param (128)
byte=0x3B state: CsiParam → CsiParam action: param_separator → params=38,2,255,128
byte=0x30 state: CsiParam → CsiParam action: param (0)
byte=0x6D state: CsiParam → Ground action: csi_dispatch(params=38,2,255,128,0, final='m')
→ SGR: 前景色を RGB(255,128,0) に設定
byte=0x48 state: Ground → Ground action: print('H')
byte=0x65 state: Ground → Ground action: print('e')
byte=0x6C state: Ground → Ground action: print('l')
byte=0x6C state: Ground → Ground action: print('l')
byte=0x6F state: Ground → Ground action: print('o')
byte=0x1B state: Ground → Escape action: none
byte=0x5B state: Escape → CsiEntry action: none
byte=0x30 state: CsiEntry → CsiParam action: param (0)
byte=0x6D state: CsiParam → Ground action: csi_dispatch(params=0, final='m')
→ SGR: 全属性リセット
byte=0x0A state: Ground → Ground action: execute(LF)
→ 改行