お題:"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()に渡さなくとも動作する理由である。
今回のお題については、ここまで。