bashのリダイレクトとパイプとファイルディスクリプタ

最近通勤中にMan page of BASHを読むのが楽しい。
https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html
数年前なら読んでもちんぷんかんぷんだったろうが
今読むとBASHの知らない仕様やその使いどころがわかって楽しい。

BASHの中でもよく使うのに奥が深い(というか直感的なのに難しい)のが
リダイレクト、パイプと、それに関わりの深いファイルディスクリプタである。

いじくりながら理解を深めたいと思う。

環境依存のことも言ってしまうかもしれないので
とりあえずバージョン書いておきます。
GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)

まず、標準出力と標準エラー出力にそれぞれ
“stdout”と”stderr”を出力するスクリプトを書く。

#!/bin/bash

echo "stdout" # to stdout
echo "stderr" 1>&2

実行してみる。

$ ./test1.sh
stdout
stderr

標準出力をファイルに書き出してみる。

$ ./test1.sh 1> out.txt
stderr
$ cat out.txt
stdout

ファイルディスクリプタの1(以降fd1)をout.txtに出力しているので
端末にはstderrだけが出て、stdoutはファイルに出力されている。

次に標準エラー出力をファイルに出してみる。

$ ./test1.sh 2> out.txt
stdout
$ cat out.txt
stderr

ファイルディスクリプタの2(以降fd2)をout.txtに出力しているので
端末への出力とファイルの内容は先ほどと逆になる。

次に、標準出力も標準エラーも両方ファイルに出力する。

$ ./test1.sh 1> out.txt 2>&1
$ cat out.txt
stdout
stderr

リダイレクトは左から順に解釈される。
ファイルディスクリプタの出力先を「->」で表すとこうなる。

[初期状態]
fd1 -> 端末
fd2 -> 端末

[1> out.txt](fd1の出力先をout.txtにする)
fd1 -> out.txt
fd2 -> 端末

[2>&1](fd2にfd1を複製する)
fd1 -> out.txt
fd2 -> out.txt
となる。

ここまでは基本的なことしかやってないのだが
既に1つの疑問が出てくる。
さっきの書き方で、test1.shの2行目の処理を書いてみると
[初期状態]
fd1 -> 端末
fd2 -> 端末

[1>&2](fd1にfd2を複製する)
fd1 -> 端末
fd2 -> 端末

、、、最初と変わってない。。。
つまり何も起きなかったことになる。

落ち着いてもう少し考えてみる。
リダイレクトは左から処理されるとman bashに書いてあるが、
スクリプトの中と外のリダイレクトとはどちらが先に処理されるのか?

こんな実験をしてみる。

$ ls -l /dev/fd/1
lrwx------ 1 ec2-user ec2-user 64 Oct 26 12:44 /dev/fd/1 -> /dev/pts/1
$ ls -l /dev/fd/2
lrwx------ 1 ec2-user ec2-user 64 Oct 26 12:44 /dev/fd/2 -> /dev/pts/1

fd1とfd2は両方とも端末を指している。
次にこんなシェルスクリプトを書く。

#!/bin/bash

ls -l /dev/fd/1
ls -l /dev/fd/2

これをリダイレクトとともに実行する。

$ ./test2.sh > fd1_out.txt 2> fd2_out.txt
$ cat fd1_out.txt
l-wx------ 1 ec2-user ec2-user 64 Oct 26 12:57 /dev/fd/1 -> /home/ec2-user/test/fd1_out.txt
l-wx------ 1 ec2-user ec2-user 64 Oct 26 12:57 /dev/fd/2 -> /home/ec2-user/test/fd2_out.txt

シェルスクリプト内部ではすでにfd1とfd2の指す先が変わっている。

さっきのtest1.shのfd複製で何も起きない疑問を
もう一度考えてみるとこうなる。
$ ./test1.sh 2> out.txt
を実行した場合

[test1.sh開始時]
fd1 -> 端末
fd2 -> out.txt

[1>&2](fd1にfd2を複製)
fd1 -> out.txt
fd2 -> out.txt
これで無事に”stderr”はout.txtに出力される。

つまり、リダイレクトはコマンドの実行前に処理されて、
コマンドを実行しているコンテキストの
fdが指す先の初期状態になる、ということ。

次はパイプの処理を見ていく。
man bashのパイプの説明は次のとおりである。

command | command2
command の標準出力は command2 の標準入力にパイプで接続されます。
この接続は、コマンドで指定したどのリダイレクションよりも先に実行されます。

ここで標準出力と標準エラーを両方パイプに渡すよくある例。
標準入力を標準出力とファイル両方に出すteeコマンドを使って
両方パイプに渡せていることを確認。

$ ./test1.sh 2>&1 | tee out.txt
stdout
stderr
$ cat out.txt
stdout
stderr

これを先ほどの書き方で説明すると
[初期状態]
fd1 -> 端末
fd2 -> 端末

[パイプ接続]
fd1 -> fd0 @ tee
fd2 -> 端末

[2>&1]
fd1 -> fd0 @ tee
fd2 -> fd0 @ tee

fd1とfd2をlsするtest2.shを実行するとこうなる。

$ ./test2.sh | tee out.txt
l-wx------ 1 ec2-user ec2-user 64 Oct 26 13:26 /dev/fd/1 -> pipe:[10890]
lrwx------ 1 ec2-user ec2-user 64 Oct 26 13:26 /dev/fd/2 -> /dev/pts/1
$ ./test2.sh 2>&1 | tee out.txt
l-wx------ 1 ec2-user ec2-user 64 Oct 26 13:26 /dev/fd/1 -> pipe:[10907]
l-wx------ 1 ec2-user ec2-user 64 Oct 26 13:26 /dev/fd/2 -> pipe:[10907]

もう少しパイプの話。
man bashには次のように書いてある。
|& を使うと、command の標準エラー出力もパイプを通して
command2 の標準入力に接続されます。 これは 2>&1 | の短縮形です。

|& は標準出力も標準エラーもパイプにつなげてくれるんだ、、
と理解すると誤った使い方をしてしまう。

$ ./test1.sh 1> stdout.txt |& cat
$ cat stdout.txt
stdout
stderr

一見、標準出力だけファイルに出力して
標準エラーはcatに渡せるように見えるが、
両方ともファイルに出力される。

man bashの説明のとおり展開してみれば一目瞭然。

$ ./test1.sh 1> stdout.txt 2>&1 | cat

慣れている人なら見たらすぐわかると思うが順序を追っていくと、
まずパイプが一番最初に処理され、
残りのリダイレクトが左から処理されるので
[初期状態]
fd1 -> 端末
fd2 -> 端末

[パイプ接続]
fd1 -> fd0 @ cat
fd2 -> 端末

[1> stdout.txt]
fd1 -> stdout.txt
fd2 -> 端末

[2>&1]
fd1 -> stdout.txt
fd2 -> stdout.txt

当然、catには何も渡らない。

標準出力はファイル、標準エラーはパイプに渡したいならこうなる。

$ ./test1.sh 2>&1 > out.txt | cat
stderr
$ cat out.txt
stdout

標準出力と標準エラー出力を入れかえる、という例が説明されていることもある。
こんな感じ。

$ ./test1.sh 3>&2 2>&1 1>&3 | tee out.txt
stdout
stderr
$ cat out.txt
stderr

これについても、fd1もfd2も端末を指しているのだから、
入れ替えても同じでは??
って思っていたが、パイプが先に接続されることが分かっていれば
先ほどの「->」記法で説明できる。

でもこの例の場合、最終的に下記の状態を作りたいのだから
fd1 -> 端末
fd2 -> パイプ
下記の方法が一番手っ取り早いのでは。
※ttyについてあまりちゃんと理解していないが。。

$ ./test1.sh 2>&1 1>/dev/tty | tee out.txt
stdout
stderr
$ cat out.txt
stderr

色々書いてみたが、実際に使うときはこんな色々考えず、
直感的な動作をするリダイレクトって、すごいなぁ。。