home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/07, clear(1)

C言語系/「デーモン君のソース探検」読書メモ/07, clear(1)

C言語系 / 「デーモン君のソース探検」読書メモ / 07, clear(1)
id: 549 所有者: msakamoto-sf    作成日: 2010-01-12 18:50:29
カテゴリ: BSD C言語 

お題:"clear"コマンドの中身を追跡せよ。


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()から見ていく。

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~"の整理

"term~"というのを良く目にするが、今の段階では以下のように整理しておく。

  1. termios構造体に関連するのは、端末それ自体に関連する設定と関わる。
  2. termcapやterminfo, curses系は、端末それ自体というよりは端末の"見た目"制御に関わる。カーソル移動や画面クリアなど。

混乱を避ける為、termcap/terminfo/cursesの変遷について極簡単にまとめておく。

  1. termcapというのは"terminal capability"の略。元々はBSDでviを作る為に構築されたデータベースファイルと、それを処理するルーチン群。
  2. その後、viから関連するルーチンが抽出され、cursesライブラリに集約された。
  3. さらにその後、termcapでの問題点を改善したterminfoファイルが構築された。
  4. AT&T System V UNIX Release 2.0 (SVR2)では、termcapの機能がterminfoに置き換えられた。
  5. 以上のような流れで、歴史的にはSystemV系はterminfoを使い、BSD系はtermcapを使う流れが出来上がった。現在はSystemV/BSD系ともに、termcap/terminfo両方をサポートするようになっている。(Mac OS Xについては定かではない)

"terminal capability"という名前から分かるように、termcap/terminfoは端末それ自体の制御は行えず、「その端末で何が出来るか、それをするにはどういう制御文字を出力すればいいのか」というデータベースライブラリとなっている。
詳細は、以下のman/Wikipedia/書籍を参照。

tput.c の main()関数後半を追ってみる。

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()関数を追ってみる。

実際にエスケープシーケンスを出力しているのは、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()の小さな謎とソース探検

ここで少し寄り道し、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()に渡さなくとも動作する理由である。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-01-12 18:55:17
md5:13d156c6684d567cdd9ed78b68b305cd
sha1:b2731f45dbcbaca5b57fa44c10b6d8d5bfa9e9f7
コメント
コメントを投稿するにはログインして下さい。