[bash] Linuxでリダイレクトがよくわからなくなったときの確認

2017年10月30日月曜日

bash linux

標準出力と標準エラー出力のリダイレクト

Linuxで標準出力や標準エラー出力をファイルに保存するのにリダイレクトさせたり、ファイル保存しつつ、端末表示するときにteeコマンドを使ったりするのは日常的にあるだろう。
でも、bashで複雑なリダイレクトをしていると、どのように動作しているのかよくわからなくなってくるときがある。
こんなときは /proc/self/fd を表示させて、確認しよう。

例えば、標準出力と標準エラー出力をそれぞれ、別のファイル(out.log, err.log)に出力しつつ、端末表示させたい場合は次のような書き方ができる。
$ { { somecommand | tee out.log 1>&3; } 2>&1 | tee err.log; } 3>&1

これがどのように動作しているのか、順を追って確認してみよう。

/proc/self/fd

まずは単純に"ls -l /proc/self/fd"を実行してみる。
$ ls -l /proc/self/fd
total 0
lrwx------ 1 user users 64 Sep 10 09:23 0 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 09:23 1 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 09:23 2 -> /dev/pts/0
lr-x------ 1 user users 64 Sep 10 09:23 3 -> /proc/2773/fd

ファイルディスクリプタ(fd)0/1/2がすべて仮想ターミナル(pts0)に接続されていることがわかる。
fd3はおそらく、ls自身がファイルシステムにアクセスするためにオープンしているものだろう。

commandA 3>&1

まず一番外側は commandA 3>&1 の形になっている。
commandAの位置で、/proc/self/fdを見ると
$ { ls -l /proc/self/fd; } 3>&1
total 0
lrwx------ 1 user users 64 Sep 10 10:42 0 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 10:42 1 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 10:42 2 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 10:42 3 -> /dev/pts/0
lr-x------ 1 user users 64 Sep 10 10:42 4 -> /proc/2766/fd

この状態ではfd3がオープンされて、端末に接続されていることがわかる。

{ commandB 2>&1 | commandC } 3>&1

今度は { commandB 2>&1 | commandC } 3>&1 の形になっている。
commandBとcommandCの位置で見てみよう。
$ { { ls -l /proc/self/fd 2>&1; } | tee err.log; } 3>&1
total 0
lrwx------ 1 user users 64 Sep 10 10:50 0 -> /dev/pts/0
l-wx------ 1 user users 64 Sep 10 10:50 1 -> pipe:[30931]
l-wx------ 1 user users 64 Sep 10 10:50 2 -> pipe:[30931]
lrwx------ 1 user users 64 Sep 10 10:50 3 -> /dev/pts/0
lr-x------ 1 user users 64 Sep 10 10:50 4 -> /proc/2854/fd
$ { { :  2>&1; } | ls -l /proc/self/fd; } 3>&1
total 0
lr-x------ 1 user users 64 Sep 10 10:51 0 -> pipe:[31063]
lrwx------ 1 user users 64 Sep 10 10:51 1 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 10:51 2 -> /dev/pts/0
lrwx------ 1 user users 64 Sep 10 10:51 3 -> /dev/pts/0
lr-x------ 1 user users 64 Sep 10 10:51 4 -> /proc/2863/fd

commandBでは、fd2がfd1の方向へリダイレクトされているので、標準出力と標準エラー出力が両方ともパイプに接続されている。
commandCでは、標準入力がパイプに接続されて、それ以外は端末に接続されていることがわかる。
つまり、commandBの標準出力/標準エラー出力はパイプを通じて、commandCに渡されて、端末に出力される。
実際にはcommandCの位置にはteeがいるためにerr.logファイルにもこの出力がコピーされる。

{ { commandD | commandE 1>&3; } 2>&1 | tee err.log; } 3>&1

最後は { { commandD | commandE 1>&3; } 2>&1 | tee err.log; } 3>&1 だ。
これも commandDとcommandEの2ヶ所で見てみよう。
$ { { ls -l /proc/self/fd | tee out.log 1>&3; } 2>&1 | tee err.log; } 3>&1
total 0
lrwx------ 1 user users 64 Sep 10 10:58 0 -> /dev/pts/0
l-wx------ 1 user users 64 Sep 10 10:58 1 -> pipe:[35158]
l-wx------ 1 user users 64 Sep 10 10:58 2 -> pipe:[34499]
lrwx------ 1 user users 64 Sep 10 10:58 3 -> /dev/pts/0
lr-x------ 1 user users 64 Sep 10 10:58 4 -> /proc/2891/fd
$ { { : | ls -l /proc/self/fd 1>&3; } 2>&1 | tee err.log; } 3>&1
total 0
lr-x------ 1 user users 64 Sep 10 11:00 0 -> pipe:[34647]
lrwx------ 1 user users 64 Sep 10 11:00 1 -> /dev/pts/0
l-wx------ 1 user users 64 Sep 10 11:00 2 -> pipe:[35269]
lrwx------ 1 user users 64 Sep 10 11:00 3 -> /dev/pts/0
lr-x------ 1 user users 64 Sep 10 11:00 4 -> /proc/2949/fd

commandDでも、標準出力と標準エラー出力が両方ともパイプに接続されている。
ただし、この場合はパイプのIDが異なっていることに注意してほしい。
fd1の接続先はcommandDの直後のパイプだけど、fd2の接続先は先ほどのcommandBとcommandCを結んでいるパイプだ。
このため、commndEにはcommandDの標準出力のみがパイプを通じて流れる。
commandEは標準入力と標準エラー出力がパイプに接続されている。

1>$3の役割は?

もう1つ重要なポイントは元々のコマンド(commandD)の標準出力の内容がerr.logに出ていないこと。
これはcommandEで 1>&3 のリダイレクトをしていることによる。
もし、このリダイレクトをしていないと、以下のような結果になる。
$ { { : | ls -l /proc/self/fd; } 2>&1 | tee err.log; } 3>&1
total 0
lr-x------ 1 user users 64  Sep 10 11:09 0 -> pipe:[29522]
l-wx------ 1 user users 64  Sep 10 11:09 1 -> pipe:[30791]
l-wx------ 1 user users 64  Sep 10 11:09 2 -> pipe:[30791]
lrwx------ 1 user users 64  Sep 10 11:09 3 -> /dev/pts/0
lr-x------ 1 user users 64  Sep 10 11:09 4 -> /proc/2797/fd

この場合、fd1がfd2と同じパイプに接続されてしまう。
そうすると、そのまま標準出力の内容がerr.logにも出力されてしまうので、teeでout.logに出力するタイミングで端末に流してしまっている。
そのために、わざわざfd3をオープンして、fd1をパイプ接続した後に 1>&3 でリダイレクトしているわけだ。

"ls -l /proc/self/fd"だけでは対応できないような場面もあるかもしれないけど、これだけでもかなりのことがわかるんじゃないかな。