#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言語系|