#navi_header|C言語系| お題:"clear"コマンドの中身を追跡せよ。 #more|| #outline|| ---- * clearの実体はtputコマンド 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()の二つ。それぞれ、少しずつソースを追っていきたい。 ** tput.c の main()関数前半を追ってみる。 まずmain()から見ていく。 #pre||> 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関数で、次のようになっている。 #pre||> 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~"の整理 "term~"というのを良く目にするが、今の段階では以下のように整理しておく。 + termios構造体に関連するのは、端末それ自体に関連する設定と関わる。 + termcapやterminfo, curses系は、端末それ自体というよりは端末の"見た目"制御に関わる。カーソル移動や画面クリアなど。 混乱を避ける為、termcap/terminfo/cursesの変遷について極簡単にまとめておく。 + termcapというのは"terminal capability"の略。元々はBSDでviを作る為に構築されたデータベースファイルと、それを処理するルーチン群。 + その後、viから関連するルーチンが抽出され、cursesライブラリに集約された。 + さらにその後、termcapでの問題点を改善したterminfoファイルが構築された。 + AT&T System V UNIX Release 2.0 (SVR2)では、termcapの機能がterminfoに置き換えられた。 + 以上のような流れで、歴史的にはSystemV系はterminfoを使い、BSD系はtermcapを使う流れが出来上がった。現在はSystemV/BSD系ともに、termcap/terminfo両方をサポートするようになっている。(Mac OS Xについては定かではない) "terminal capability"という名前から分かるように、termcap/terminfoは端末それ自体の制御は行えず、「その端末で何が出来るか、それをするにはどういう制御文字を出力すればいいのか」というデータベースライブラリとなっている。 詳細は、以下のman/Wikipedia/書籍を参照。 - "termios"関連 -- man 4 termios -- man 3 tcsetattr - "termcap", "terminfo", "curses" -- man 3 termcap -- man 5 termcap -- man 5 terminfo -- man 3 curses -- http://en.wikipedia.org/wiki/Termcap -- http://en.wikipedia.org/wiki/Terminfo -- http://en.wikipedia.org/wiki/Curses_%28programming_library%29 -- &amazon() {Termcap and Terminfo (Nutshell Handbooks)} ** tput.c の main()関数後半を追ってみる。 main()関数の続きに戻る。 #pre||> 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()関数を追ってみる。 実際にエスケープシーケンスを出力しているのは、tput.c中の process() 関数になる。 #pre||> 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までとなる。 #pre||> /* 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関数を渡している。 #pre||> static int outc(c) int c; { return (putchar(c)); } ||< 以上で"clear"コマンドが画面をクリアするエスケープシーケンスを出力する仕組みを解明することができた。 * tgetent(), tgetstr()の小さな謎とソース探検 ここで少し寄り道し、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()両方とも定義されていた。 #pre||> 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() に委譲していることが分かる。 #pre||> 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()に渡さなくとも動作する理由である。 今回のお題については、ここまで。 #navi_footer|C言語系|