ShellScript
Bash

bashでストリームデータ処理

More than 3 years have passed since last update.

パイプ、名前付きパイプ、Process Substitutionなどを使うとbashでそれなりに複雑なストリームデータ処理を組むことができます。

基本

$ cmd1 | cmd2 | cmd3

パイプでつなげば前段のコマンドの標準出力と後段のコマンドの標準入力が接続されます。

$ cmd < input > output

<でファイルや名前付きパイプ(named pipe, fifo)から標準入力へデータを流すことでき、>で標準出力をファイルや名前付きパイプへ流すことができます。

分配

一つのデータストリームを複数のプロセスに分配するパターンです。「放送(ブロードキャスト)」と言ってもいいかもしれません。

基本的にはteeコマンドで実現できます。

名前付きパイプを使う場合

$ mkfifo p
$ tee p < input | cmd1 & cmd2 < p

teeによってinputストリームを名前付きパイプp及び標準出力へ出力します。cmd1teeの標準出力から、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は全ての読み出し可能な入力ストリームからの行データが揃うのを待ってから出力を行います。そのため、例えばcmd1cmd2で出力スループットが異なる場合、遅い方にペースを合わせて速い方がブロックすることになります。

参考