パイプ、名前付きパイプ、Process Substitutionなどを使うとbashでそれなりに複雑なストリームデータ処理を組むことができます。
基本
$ cmd1 | cmd2 | cmd3
パイプでつなげば前段のコマンドの標準出力と後段のコマンドの標準入力が接続されます。
$ cmd < input > output
<
でファイルや名前付きパイプ(named pipe, fifo)から標準入力へデータを流すことでき、>
で標準出力をファイルや名前付きパイプへ流すことができます。
分配
一つのデータストリームを複数のプロセスに分配するパターンです。「放送(ブロードキャスト)」と言ってもいいかもしれません。
基本的にはtee
コマンドで実現できます。
名前付きパイプを使う場合
$ mkfifo p
$ tee p < input | cmd1 & cmd2 < p
tee
によってinput
ストリームを名前付きパイプp
及び標準出力へ出力します。cmd1
はtee
の標準出力から、cmd2
は名前付きパイプp
からストリームを読み込みます。
Process Substitutionを使う場合
bashの機能であるProcess Substitutionを使うと名前付きパイプを使わずに上記と同様のことを実現できます。
$ tee >(cmd2) < input | cmd1
>()
の部分がProcess Substitutionです。
bashはこれを見ると、一時的な名前付きパイプを生成し、そのパイプを標準入力として中のcmd2
を実行し、>()
の部分自体はその名前付きパイプのパスに置き換わります。(ただし、環境によってはパイプじゃなくてファイルだったり、それへのシンボリックリンクだったりするかもしれません。詳細はmanpageを参照)
もちろん、複数のプロセスへ分配することも可能です。
$ tee >(cmd1) >(cmd2) >(cmd3) | cmd4
参考
集約
複数のデータストリームを束ねて一つのプロセスで処理するパターンです。分配に比べるといろいろトリッキーな問題が出てきます。
各入力ストリームを順番に処理すればいい場合
この場合は比較的簡単です。
サブシェルを使うのもよし。
$ ( cmd1 ; cmd2 ) | cmd3
$ { cmd1 ; cmd2; } | cmd3
(()
と{}
の違いについてはmanpageなどを参照)
cat
とProcess Substitutionを使うこともできます。
$ cat <(cmd1) <(cmd2) | cmd3
ただし、いずれの場合もcmd1
のデータを全て読み出した後でないとcmd2
のデータが読み出されないことに注意が必要です。そのため、cmd1
が永続的にデータを出力し続けるようなプロセスの場合はうまくいきません。
全ての入力ストリームから同時にデータを吸い上げたい場合
以下はダメな例です。
$ (cmd1 & cmd2) | cmd3
この方法では、cmd1
の出力とcmd2
の出力が意図しないところで混ざってcmd3
に渡される可能性があります。「どういう混ざり方をしてもOK」という場合ならこれでもいいですが、そういうケースは稀でしょう。
試しに以下のようなコマンドを実行してみます。
$ ( perl -e 'print "a"x1024 . "\n" for 1..100' & \
perl -e 'print "b"x1024 . "\n" for 1..100' ) | \
perl -nle 'print if !/^(.)\1*$/'
上記におけるcmd1
は「1024文字の"a"を100行出力する」というもの、cmd2
はその"b"バージョンです。cmd3
は"a"と"b"が混ざっている行のみを表示します。環境にもよるかもしれませんが、何件か行が混ざっているケースがあるはずです。
このように、行単位で出力を区切って欲しい場合はfdlinecombine
が便利そうです。
fdlinecombine
は、複数ストリームを同時に読み込みつつ、行単位で出力を区切って混ざらないようにしてくれます。
$ fdlinecombine <(cmd1) <(cmd2) | cmd3
また、SEPARATOR
環境変数を設定することで改行以外のデータをメッセージ境界とすることができるようです。(設定方法が若干トリッキーなので要注意)
pasteを使ってみる
fdlinecombine
は便利なのですが、どこにでもインストールされているわけではないのがキビシイところです。
入力と出力の条件次第ですが、paste
コマンドで集約できるケースもあります。
$ paste -d \\n <(cmd1) <(cmd2) | cmd3
paste
コマンドは引数で指定したストリームを同時に読みだして、それらの各行を「横に並べて」出力します。デフォルトではTABで並べて出力されますが、-d
オプションでデリミタを改行コードに変えることで1本のストリームにまとめることができます。
paste
を使う場合は以下の2点に注意が必要です。
-
paste
では入力ストリームは行ごとに処理されるのみであり、任意のメッセージ境界を設定することはできません。 -
paste
は全ての読み出し可能な入力ストリームからの行データが揃うのを待ってから出力を行います。そのため、例えばcmd1
とcmd2
で出力スループットが異なる場合、遅い方にペースを合わせて速い方がブロックすることになります。
参考
- http://qiita.com/kawaz/items/98768ba5eea8d6fe6a19
- http://serverfault.com/questions/171095/how-do-i-join-two-named-pipes-into-single-input-stream-in-linux
- http://askubuntu.com/questions/133386/how-to-merge-and-pipe-results-from-two-different-commands-to-single-command
- http://stackoverflow.com/questions/16867414/can-i-take-an-output-stream-duplicate-it-with-tee-munge-one-of-them-and-pipe