teeコマンドをパイプ接続したときの挙動

teeコマンドの挙動でハマったのでメモ。

シェルスクリプト内でteeコマンドを次のような感じで使っていたら、out1.txtをcatする部分でファイル無しエラーになるケースがあった。

cat hogehoge     |
(なんかの処理)   |
tee out1.txt     |
(続きの処理)     > out2.txt

cat out1.txt  |
...

(続きの処理)がシンプルなほどこの現象が起きやすい。どうも、teeによるout1.txtの書き込みが完了するよりも前に、out2.txtの出力が終わって次のcatにまで進んでしまっているような感じ。teeは標準出力と指定ファイルの両方に同じ内容のデータを出力するわけだけど、標準出力からのパイプ処理はファイルへの出力の完了を待ってくれるわけではないっぽい。

気になったのでtee.cのソースを見てみる。

まずLIST構造体が定義されてる。これは出力対象のファイルディスクリプタを連結リスト形式で保持するためのものっぽい。

typedef struct _list {
	struct _list *next;
	int fd;
	const char *name;
} LIST;
LIST *head;

add()関数は、ファイルディスクリプタとファイル名を受け取ってLITS構造体に格納し、そのheadをひとつ前のLISTのtailとつなぐ。つまり、指定された出力ファイルを連結リストの末尾に追加する。。

void
add(fd, name)
	int fd;
	const 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;
}


main()の方では、ファイルの数だけadd()を実行するループ処理になっているけれど、標準出力のディスクリプタはリストの先頭に付く。

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);

ということは、ファイルの出力よりもパイプへの出力の方が順番的には早いということ。パイプの先つながったコマンドは、前のコマンドの出力が終わらなくてもサブプロセス化されてどんどん実行されてしまうので、(続きの処理)が軽ければout2.txtへの出力が先に完了するような状況はあり得そう。

ただ、ファイルディスクリプタのclose()は、全ファイルへの出力が完了してからまとめて実行されるようになってるから、out1.txtのcatが先行するのはちょっと腑に落ちない感じがする。でもとりあえずteeの挙動には気をつけたほうがいいみたい。