これまでKiloのソースコードであるkilo.cの主な処理の流れがどのようになっているのかを説明し、さらにソースコードを理解するために必要になるマクロや構造体といった機能を説明した。今回はKiloのソースコードで特に興味深い部分であるエスケープシーケンスについて紹介する。

まず、kilo.cの290行目あたりのコードを読んでみよう。getCursorPosition()という関数が定義されているのだが、この関数はエスケープシーケンスを使って現在カーソルが存在している位置(行、列)を取得するというものだ。

kilo.cで最初にエスケープシーケンスが使われている部分 (290行目あたり)

/* Use the ESC [6n escape sequence to query the horizontal cursor position

 * and return it. On error -1 is returned, on success the position of the
 * cursor is stored at *rows and *cols and 0 is returned. */
int getCursorPosition(int ifd, int ofd, int *rows, int *cols) {
    char buf[32];
    unsigned int i = 0;

/* Report cursor location */
if (write(ofd, "\x1b[6n", 4) != 4) return -1;

/* Read the response: ESC [ rows ; cols R */
while (i < sizeof(buf)-1) {
    if (read(ifd,buf+i,1) != 1) break;
    if (buf[i] == 'R') break;
    i++;
}
buf[i] = '\0';

/* Parse it. */
if (buf[0] != ESC || buf[1] != '[') return -1;
if (sscanf(buf+2,"%d;%d",rows,cols) != 2) return -1;
return 0;

}

上記ソースコードの「write(ofd, "\x1b[6n", 4)」という部分に注目してほしい。この「\x1b[6n」はエスケープシーケンスと呼ばれている。\xは16進数表記を意味しているので、これは「0x1b」に「[6n」がそのまま続いているデータということになる。ascii(7)をマニュアルで引いて見ると、次のように0x1bはESC(エスケープ)が割り当てられていることがわかる。

ascii(7)オンラインマニュアルで0x1bがESCであることを確認

エスケープから始まる文字列を標準出力に出力すると、ターミナルはそれを制御コードであると解釈して割り当てられている機能を実行する仕組みになっている。先ほどの「\x1b[6n」は「現在カーソルがあるポジション(行、列)を教えてくれ」という命令になっており、それを使ってカーソルの場所情報を取得する関数ということになる。

試しに、エスケープシーケンスを出力するだけの簡単なプログラムを組んで確認してみよう。次のようなプログラムをC言語を使って書いてみる。コンパイルして実行すると、次のようにカーソルの位置情報が出力されることがわかる。;の前の数字が行で後ろの数字が列を表している。

カーソルがある場所を確認する getcursorposition.c

#include <stdio.h>

include 

int
main(int argc, char *argv[])
{
    printf("\x1b[6n");

return(EX_OK);
}

コンパイルして実行。赤い文字がターミナルからの出力

行を変えると返ってくる値が変化する

カーソルのある行がそのまま戻ってくる

これはC言語を使う必要があるわけではなく、コマンドで同じようにエスケープシーケンスを出力すれば機能する

この機能はC言語によるプログラミングが必須というわけではなく、どのような方法であれエスケープシーケンスを出力することで機能する。例えば、上記のようにprintf(1)というコマンドを使ってエスケープシーケンスを出力すると、作成したプログラムと同じような結果を得ることができる。

kilo.cでほかにエスケープシーケンスを使っている部分を抜粋してみよう。こうして使われているエスケープシーケンスを抜き出すと、Kiloがどのようなことをしているのかが見えてくる。

330行目あたり。カーソルを最果てに移動させることでターミナルのサイズを推測するというテクニック

    /* Go to right/bottom margin and get position. */
    if (write(ofd,"\x1b[999C\x1b[999B",12) != 12) goto failed;
    retval = getCursorPosition(ifd,ofd,rows,cols);
    if (retval == -1) goto failed;

865行目あたり。一旦カーソルを消してからカーソルを移動させるというテクニック

abAppend(&ab,"\x1b[?25l",6); /* Hide cursor. */
abAppend(&ab,"\x1b[H",3); /* Go home. */

870行目あたり。行末までクリアするエスケープシーケンスを入れて表示を綺麗にするテクニック

            char welcome[80];
            int welcomelen = snprintf(welcome,sizeof(welcome),
                "Kilo editor -- verison %s\x1b[0K\r\n", KILO_VERSION);

900行目あたりから:表示する文字を装飾

                abAppend(&ab,"\x1b[7m",4);

...略...
                    abAppend(&ab,"\x1b[0m",4);
...略...
                        abAppend(&ab,"\x1b[39m",5);
...略...
                        int clen = snprintf(buf,sizeof(buf),"\x1b[%dm",color);
...略...
        abAppend(&ab,"\x1b[39m",5);
        abAppend(&ab,"\x1b[0K",4);
...略...
    abAppend(&ab,"\x1b[0K",4);
    abAppend(&ab,"\x1b[7m",4);
...略...
    abAppend(&ab,"\x1b[0m\r\n",6);
...略...
    abAppend(&ab,"\x1b[0K",4);
...略...
    snprintf(buf,sizeof(buf),"\x1b[%d;%dH",E.cy+1,cx);
    abAppend(&ab,buf,strlen(buf));
    abAppend(&ab,"\x1b[?25h",6); /* Show cursor. */

Kiloで使われているエスケープシーケンスを整理すると次のようになる。

Kiloで使われているエスケープシーケンス

0x1b [6n カーソルの場所(行,列)を取得
0x1b [999C カーソルを右へ999個移動
0x1b [999B カーソルを下へ999個移動
0x1b [?25l カーソルを非表示
0x1b [?25h カーソルを表示
0x1b [H カーソルをホームへ移動
0x1b [0K カーソルから行末までを削除
0x1b [7m 背景色と前景色の入れ換え
0x1b [0m 属性のクリア
0x1b [39m デフォルトに戻る

通常はこうしたエスケープシーケンスを利用してさらに抽象化されたAPIを提供しているライブラリを使ったりするのだが(互換性などの面倒なところをなんとなく吸収してくれる)、実際にはこうやってエスケープシーケンスを出力してターミナルと対話しつつ処理が行われている(専用の関数が使われているところもある)。

今回はこの辺りにしておこう。エスケープシーケンスというエスケープ(0x1b)から始まる一連の文字列には特別な意味があり、ターミナルが出力を受けるとそれに対応した機能を発動する、ということを理解してもらえればと思う。次回は、このエスケープシーケンスを使ってどんなことを実現されているのかを詳しく説明する。