お題:tee(1)の仕組みを追跡せよ
※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。
tee(1)コマンドはパイプと組み合わせ、実行結果を画面とファイル、両方に出力したい場合に使われる。script(1)と似ている。
script(1)が2段にfork(2)して標準入出力全てをファイルにコピーするのに対し、tee(1)はSTDINからの入力ををSTDOUTおよびコマンドラインで指定されたファイルへ出力する橋渡し機能だけに特化している。
teeコマンドはmakeコマンドと組み合わせて使われる例が多い。またWindows PowerShellでも同様の機能が提供されているらしい。
実行ファイルとソースファイルの場所を把握する。
$ which tee /usr/bin/tee $ locate tee.c /usr/pkgsrc/shells/bash2/work/bash-2.05/examples/loadables/tee.c /usr/src/usr.bin/tee/tee.c
それではtee.cを読んでみる。コンパクトなのでサクサク進めていく。
まず、出力先ファイル群のファイル記述子をリスト構造で保持する為の構造体定義と先頭ポインタの宣言がある。
typedef struct _list {
struct _list *next;
int fd; /* ファイル記述子 */
char *name; /* argvから取得したファイル名 */
} LIST;
LIST *head;
続いてmain()関数を見てみる。変数宣言に続いてgetopt(3)によるオプション解析、続いてBSIZEでdefineされたサイズのバッファ領域がmalloc(3)で確保される。
int
main(argc, argv)
int argc;
char *argv[];
{
LIST *p;
int n, fd, rval, wval;
char *bp;
int append, ch, exitval;
char *buf;
#define BSIZE (8 * 1024)
/* ロカールを初期化 */
setlocale(LC_ALL, "");
append = 0;
while ((ch = getopt(argc, argv, "ai")) != -1)
switch((char)ch) {
case 'a':
/* "-a"ならファイルへ追記する */
append = 1;
break;
case 'i':
/* "-i"ならSIGINTを無視する */
(void)signal(SIGINT, SIG_IGN);
break;
case '?':
default:
(void)fprintf(stderr, "usage: tee [-ai] [file ...]\n");
exit(1);
}
argv += optind;
argc -= optind;
/* バッファ領域の確保 */
if ((buf = malloc((size_t)BSIZE)) == NULL)
err(1, "malloc");
その後、出力先のファイルオープン処理が続く。
add(STDOUT_FILENO, "stdout");
for (exitval = 0; *argv; ++argv)
if ((fd = open(*argv, append ? O_WRONLY|O_CREAT|O_APPEND :
O_WRONLY|O_CREAT|O_TRUNC, DEFFILEMODE)) < 0) {
warn("%s", *argv);
exitval = 1;
} else
add(fd, *argv);
先にadd()関数を見てみる。
void
add(fd, name)
int fd;
char *name;
{
LIST *p;
if ((p = malloc((size_t)sizeof(LIST))) == NULL)
err(1, "malloc");
p->fd = fd;
p->name = name;
p->next = head;
head = p;
}
ファイル記述子とファイル名を引数で受け取り、LIST構造体をmalloc(3)で確保、headから辿るリスト構造に連結している。
p->next = head; head = p;
この2行がぱっと見ただけでは分かりづらい。初期状態ではheadはLIST構造体のポインタでNULLを指していることを思い出すと何をしているか想像できるようになる。イメージとしては下図のようになる。
LIST *head -> NULL
↓add(...)
LIST *head -> +-------+
| fd |
| name |
| next |--> NULL
+-------+
↓add(...)
LIST *head -> +-------+
| fd |
| name |
| next |-->+-------+
+-------+ | fd |
| name |
| next |--> NULL
+-------+
↓add(...)
...
main()関数に戻れば、最初にSTDOUTのファイル記述子をリストに加え、
add(STDOUT_FILENO, "stdout");
コマンドラインで指定されたファイルを順にopen(2)していき、リストに加えていく。また"-a"オプションが指定された場合は追記になるよう、"O_APPEND"フラグを指定している。
for (exitval = 0; *argv; ++argv)
if ((fd = open(*argv, append ? O_WRONLY|O_CREAT|O_APPEND :
O_WRONLY|O_CREAT|O_TRUNC, DEFFILEMODE)) < 0) {
warn("%s", *argv);
exitval = 1;
} else
add(fd, *argv);
続くwhileループが、STDINをSTDOUT + ファイル群にコピーするループになる。read(2)で読み込んだ分だけ、それぞれのファイル記述子に対してwrite(2)している。
while ((rval = read(STDIN_FILENO, buf, BSIZE)) > 0)
for (p = head; p; p = p->next) {
n = rval;
bp = buf;
do {
if ((wval = write(p->fd, bp, n)) == -1) {
warn("%s", p->name);
exitval = 1;
break;
}
bp += wval;
} while (n -= wval);
}
最後は戻り値の判定と、ファイル記述子のclose(2)を行っている。
if (rval < 0) {
warn("read");
exitval = 1;
}
for (p = head; p; p = p->next) {
if (close(p->fd) == -1) {
warn("%s", p->name);
exitval = 1;
}
}
exit(exitval);
}
以上でtee(1)の仕組みは判明した。標準出力およびコマンドラインで指定されたファイル群のファイル記述子をリスト構造で保持し、標準入力から読み込んだデータをコピーしていくようになっている。
今回のお題については、ここまで。