お題:"clear"コマンドの中身を追跡せよ。
clearは端末画面をクリアするコマンド。その中身を調べてみる。
まずはソースコードを探す。
$ locate clear ... /usr/bin/clear ... /usr/src/usr.bin/tput/clear.sh
「デーモン君のソース探検」にも書かれているとおり、本体がtputというプログラムで、clearコマンド自体はtputを呼び出すシェルスクリプトになっている。
$ which tput /usr/bin/tput $ file /usr/bin/tput /usr/bin/tput: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), \ for NetBSD, dynamically linked (uses shared libs), stripped
$ which clear /usr/bin/clear $ file /usr/bin/clear /usr/bin/clear: Bourne shell script text executable
$ cat /usr/bin/clear #!/bin/sh - # $NetBSD: clear.sh,v 1.2 1994/12/07 08:49:09 jtc Exp $ (...) exec tput clear
clearシェルスクリプトでは、"clear"というコマンドライン引数を指定してtputを実行している。
ここからtputのソース探索に進む。
$ cd /usr/src/usr.bin/tput/ $ ls CVS/ Makefile clear.sh tput.1 tput.c
tput.cの主要関数はmain()とprocess()の二つ。それぞれ、少しずつソースを追っていきたい。
まずmain()から見ていく。
int main(argc, argv) int argc; char **argv; { int ch, exitval, n; char *cptr, *p, *term, buf[1024], tbuf[1024]; term = NULL; while ((ch = getopt(argc, argv, "T:")) != -1) switch(ch) { case 'T': term = optarg; break; case '?': default: usage(); } argc -= optind; argv += optind; if (!term && !(term = getenv("TERM"))) errx(2, "no terminal type specified and no TERM environmental variable."); if (tgetent(tbuf, term) != 1) err(2, "tgetent failure"); setospeed();
まず変数の定義に続き、getopt()による引数解析。続いて端末名を取得する。コマンドライン引数で指定されていなければ"TERM"環境変数から取得している。ちなみにWindowsXPからputtyを使いSSHでログインし、ログインシェルがbashの状態では、"TERM"環境変数には "xterm" が設定されていた。
そして tgetent() にてtermcapデータベースから端末名のエントリをロードする。
続いて setospeed() という関数を呼び出している。これはtput.c内で定義されたstatic関数で、次のようになっている。
static void setospeed() { #undef ospeed extern short ospeed; struct termios t; if (tcgetattr(STDOUT_FILENO, &t) != -1) ospeed = 0; else ospeed = cfgetospeed(&t); }
結論から言うと、こちらは端末の出力速度(bps, baud rate)をtermios構造体より取り出し、termcapが公開しているospeed変数にセットしている。
"term~"というのを良く目にするが、今の段階では以下のように整理しておく。
混乱を避ける為、termcap/terminfo/cursesの変遷について極簡単にまとめておく。
"terminal capability"という名前から分かるように、termcap/terminfoは端末それ自体の制御は行えず、「その端末で何が出来るか、それをするにはどういう制御文字を出力すればいいのか」というデータベースライブラリとなっている。
詳細は、以下のman/Wikipedia/書籍を参照。
main()関数の続きに戻る。
for (exitval = 0; (p = *argv) != NULL; ++argv) { switch (*p) { case 'c': if (!strcmp(p, "clear")) p = "cl"; break; case 'i': if (!strcmp(p, "init")) p = "is"; break; case 'l': if (!strcmp(p, "longname")) { prlongname(tbuf); continue; } break; case 'r': if (!strcmp(p, "reset")) p = "rs"; break; } cptr = buf; if (tgetstr(p, &cptr)) argv = process(p, buf, argv); else if ((n = tgetnum(p)) != -1) (void)printf("%d\n", n); else exitval = !tgetflag(p); if (argv == NULL) break; } exit(argv ? exitval : 2); }
まずforループの中のswitchブロックは、"clear"/"init"/"longname"/"reset"という、tputのmanページにも書かれている特殊な引数の処理になっている。"clear"は"cl"という文字列に変換されることが分かる。
変換された文字列は"p"というcharポインタに格納され、tgetstr(),tgetnum(),tgetflag()の順にtermcapエントリの中での設定を調べていく。pが指す文字列は、termcapにおいては "capability" と呼ばれ、その端末で使える機能を英数字2文字の組み合わせで指定することになっている。"cl"だと以下のcapabilityが指定されたことになる。
clear_screen cl 画面を消去しカーソルをホームに移動 (P*)
他にも、capabilityにはブール値, 数値, 文字列型という分類があるが今回はそこまでは踏み込まない。
"cl"は "man 5 termcap" によると文字列型である。そこで実際にtermcapファイルを調べ、xtermでのエントリからclを探してみる。
/usr/share/misc/termcap:
x10term|vs100-x10|xterm terminal emulator (X10 window system):\ :am:bs:km:mi:ms:xn:xo:\ :co#80:it#8:li#65:\ :AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:RA=\E[?7l:SA=\E[?7h:\ :al=\E[L:cd=\E[J:ce=\E[K:cl=\E[H\E[2J:cm=\E[%i%d;%dH:... ... ^^^^^^^^^^^^
ここから、"cl"というcapabilityを使うには、xtermにおいては次のエスケープシーケンスを出力すればよいと予想出来る。
\E[H\E[2J
実際にprintf(1)を使うと簡単に確認出来た。
$ printf "\e[H\e[2J"
実際にエスケープシーケンスを出力しているのは、tput.c中の process() 関数になる。
static char ** process(cap, str, argv) char *cap, *str, **argv; { /* errXXXX系は省略 */ char *cp; int arg_need, arg_rows, arg_cols; /* Count how many values we need for this capability. */ for (cp = str, arg_need = 0; *cp != '\0'; cp++) if (*cp == '%') switch (*++cp) { case 'd': case '2': case '3': case '.': case '+': arg_need++; break; case '%': case '>': case 'i': case 'r': case 'n': case 'B': case 'D': break; default: /* * hpux has lot's of them, but we complain */ errx(2, erresc, *cp, cap); }
まずローカル変数を定義した後、forループでcapabilityが必要としている引数の数をカウントしている。
たとえばカーソルを移動させるcapability(ex:"cm")は行と桁の二つの数字を必要としている。そしてcmを例に挙げると、xtermでは以下のようになっている。
cm=\E[%i%d;%dH
このうち "%d" が引数で渡された数字が入るようになっているので、"cm"が必要とする引数は2つになる。他にも"%"始まりで数字を指定出来るようになっており、その仕掛けを使って引数の数をカウントしているのが上のifブロックに続くswitchブロックになる。
引数を渡すにはこの後で出てくる tgoto() を使う。そして tgoto() では数値を2つ渡せるようになっている。すなわち、続く次のコードのように引数の数は0から2までとなる。
/* And print them. */ switch (arg_need) { case 0: (void)tputs(str, 1, outc); break; case 1: arg_cols = 0; if (*++argv == NULL || *argv[0] == '\0') errx(2, errfew, 1, cap); arg_rows = atoi(*argv); (void)tputs(tgoto(str, arg_cols, arg_rows), 1, outc); break; case 2: if (*++argv == NULL || *argv[0] == '\0') errx(2, errfew, 2, cap); arg_rows = atoi(*argv); if (*++argv == NULL || *argv[0] == '\0') errx(2, errfew, 2, cap); arg_cols = atoi(*argv); (void) tputs(tgoto(str, arg_cols, arg_rows), arg_rows, outc); break; default: errx(2, errmany, arg_need, cap); } return (argv); }
上のswitchブロックで数値を0個、1個、2個とる場合に分け、1個以上の場合はargvからatoi()を使って数値を取り出している。
その後、数値を使う場合は一旦tgoto()を挟んだ後、tputs()を使って出力している。
例えば"clear"コマンドの場合はcapabilityは"cl"という文字列型になり、その値は
\E[H\E[2J
である。これには数値引数が含まれていないので、上のswitchブロックでは0個のcaseを通り、tputs()を通じて出力される。tputs()の最後の引数は関数ポインタになっており、ここではtput.c内で定義されている次のstatic関数を渡している。
static int outc(c) int c; { return (putchar(c)); }
以上で"clear"コマンドが画面をクリアするエスケープシーケンスを出力する仕組みを解明することができた。
ここで少し寄り道し、tgetent()やtgetstr()で取得される文字列の中身を見てみようと思う。tputのソース一式をホームディレクトリ以下にコピーし、端末名取得, tgetent(), tgetstr()の直後で関連する値を表示させてみる。
DEBUG: term=[xterm] DEBUG: tbuf=[xterm|vs100|xterm terminal emulator (X Window System)\ :am:bs:km:mi:ms:ut:xn:co#80:it#8:li#24:Co#8:pa#64:AB=\E[4%dm\ ... :cl=\E[H\E[2J:cm=\E[%i%d;%dH:... DEBUG: cptr=buf=[ESC[HESC[2J]
tgetent(), tgetstr()の動作を実際に確認することが出来た。
ここで、「デーモン君のソース探検」で一つの疑問が呈示される。
tput.cでは、tgetent()にて"tbuf"変数にエントリ内容が保存されている。
if (tgetent(tbuf, term) != 1) err(2, "tgetent failure");
一方、続くtgetstr()では保存されたtbuf変数が引数にあらわれていない。
cptr = buf; if (tgetstr(p, &cptr)) argv = process(p, buf, argv);
ではtgetstr()は端末のエントリをどこから読み込んでいるのか?
これを調べる為、tgetent(), tgetstr()のソースを読んでみる。
$ locate tgetent
/usr/share/man/cat3/tgetent.0
/usr/share/man/man3/tgetent.3
$ locate tgetstr
/usr/share/man/cat3/tgetstr.0
/usr/share/man/man3/tgetstr.3
ハズレである。ここで、"man 3 termcap"をもう一度見直してみると
LIBRARY Termcap Access Library (libtermcap, -ltermcap)
とある。そこで、単純に "termcap" でlocateしてみると "/usr/src/lib/libterm" というディレクトリが見つかる。
$ locate termcap ... /usr/src/lib/libterm/termcap.3 /usr/src/lib/libterm/termcap.c ...
中を見てみるといかにもそれらしきソースファイルが揃っている。
$ cd /usr/src/lib/libterm/ $ ls CVS/ Makefile TEST/ pathnames.h shlib_version termcap.3 termcap.c termcap.h termcap_private.h tgoto.c tputs.c
grepしてみる。
$ grep tgetent *.c termcap.c: * tgetent only in a) the buffer is malloc'ed for the caller and termcap.c:tgetent(bp, name)
termcap.cを見てみると、tgetent(), tgetstr()両方とも定義されていた。
int tgetent(bp, name) char *bp; const char *name; { int i, plen, elen, c; char *ptrbuf = NULL; i = t_getent(&fbuf, name); if (i == 1) { /* ... */ strcat(bp, ptrbuf); tbuf = bp;
tgetent()を見てみると、t_getent()にてファイルstaticなtinfo構造体 "fbuf" に情報を格納した後、引数で渡されたbpにエントリ内容をコピーし、最後にtbufにポインタをコピーしている。
tbuf = bp;
tbufというのもファイルstaticなcharポインタである。
static char *tbuf = NULL; /* termcap buffer */ static struct tinfo *fbuf = NULL; /* untruncated termcap buffer */
続いてtgetstr()を見てみると、fbufをそのまま、あるいはtbufをダミーのtinfo構造体にセットして t_getstr() に委譲していることが分かる。
char * tgetstr(id, area) const char *id; char **area; { struct tinfo dummy; char ids[3]; /* ... */ if ((id[0] == 'Z') && (id[1] == 'Z')) { dummy.info = tbuf; return t_getstr(&dummy, ids, area, NULL); } else return t_getstr(fbuf, ids, area, NULL); }
このように、tgetent()した段階でtermcap.c内のグローバル変数 tbuf, fbuf に内容がコピーされ、tgetstr()はそこからcapabilityの内容を調べている。これが、tput.cのmain()関数でtgetent()で取得したバッファ領域をtgetstr()に渡さなくとも動作する理由である。
今回のお題については、ここまで。