双六工場日誌

平凡な日常を淡々と綴ります。

bashのプロセス置換機能を活用して、シェル作業やスクリプト書きを効率化する

@ さんが「シェルスクリプトでハマった件→【募】ステキな回避方法」でお題を出されていて、それに回答してみました。

その内容はリンク先を見てもらうとして、回答の中で使ったbashのプロセス置換について書かれた記事をあまり見ないので、回答で使ったプロセス置換のことをエントリにしてみたいと思います。

最初に注意点ですが、プロセス置換の機能は、bashzsh*1の機能でPOSIX互換の機能ではありません。そのため、使用時には、対応していないシェルでは使えませんし、bashで使う場合も /bin/sh ではなく /bin/bash を明示的に指定する必要があります。たとえば、プロセス置換を使ったスクリプト「script.sh」に対して"$ bash script.sh" というコマンドは成功しますが、"$ sh script.sh" というコマンドは失敗します。この辺りの違いは「/bin/sh と /bin/bash の違い」を見てみてください。

さて、本題のプロセス置換ですが、"man bash” を見ると以下のように書かれています。

Process Substitution

Process substitution is supported on systems that support named pipes (FIFOs) or the /dev/fd method of naming open files. It takes the form of <(list) or >(list). The process list is run with its input or output connected to a FIFO or some file in /dev/fd. The name of this file is passed as an argument to the current command as the result of the expansion. If the >(list) form is used, writing to the file will provide input for list. If the <(list) form is used, the file passed as an argument should be read to obtain the output of list.

When available, process substitution is performed simultaneously with parameter and variable expansion, command substitution, and arith-metic expansion.

これだけではよくわからないと思うので、出力対象として使った場合、入力対象として使った場合の使用例をご紹介します。

出力対象としてプロセス置換を使う >(list)

出力対象にプロセス置換を指定すると、例えば以下のようなことができます。僕としては、一番最初の例をよく使います。

スクリプト出力のすべてをログに取る*2

#!/bin/bash
# 以降の出力はすべて画面に出力されるとともにログファイルにも記録される。
exec 1> >(tee -a stdout.log)
exec 2> >(tee -a stderr.log >&2)
(何か処理)

標準エラー出力の内容をフィルタする*3

#!/bin/bash
exec 2> >(grep -v "exclude" > &2)
(何か処理)

teeの分割先に指定

cat inputfile | tee >(grep -v exclude > outputfile)

入力対象としてプロセス置換を使う <(list)

こちら側の使い方では、処理済みテキストを渡したい場合に汎用的に使うことができます。いくつか例を挙げます。

diffの入力対象に処理済みテキストを渡す

一番よくある使い方はdiffと組み合わせて使う方法です。diffは、入力対象としてファイルのみを対象としているため、通常のパイプで処理済みテキストを渡すことができません。プロセス置換を使うことで、コマンドで処理した結果同士をdiffで比べることができます。

diff <(grep -v "exclude" inputfile) <(sed -e 's/a/b/' inputfile)

inputfileの中で、"exclude"という言葉が入っている行を除外してWhileループを回す

プロセス置換は、あたかもファイルであるかのように扱うことができるので、Whileループにも渡すことができます。たとえば、以下の2つは同等の内容になります。

  • プロセス置換を使わない方法
cat inputfile | grep -v "exclude" > newfile
while read line
do
    echo $line
done <newfile
  • プロセス置換を使う方法
while read line
do
    echo $line
done < <(cat inputfile | grep -v "exclude")

プロセス置換を使うとこのように一時ファイルへの書き出しを行う必要がなくなり、処理後に削除するなどといった扱いを気にする必要がなくなります。

bash プロセス置換」などのワードで検索するとほかにもいろいろ例が出てくると思います。どうしても一時ファイルを書きたくない、という場合以外は、必ず使わなければいけない機能ではないですが、覚えているとスクリプトをシンプルかつ見通しよく書ける場合があります。

僕も応用例にはあまり詳しくないので、ほかにいい活用例があれば教えてもらえると嬉しいです。

<追記>

プロセス置換を使ったタイムスタンプ付きフィルタの例として、OpenStack開発環境のDevStackにちょうどいい例があるので、そちらもご紹介。DevStackのスクリプトは、bashの便利機能を多用しているので非常にいいサンプルになります。 ただ、bash固有の機能を知らないとどこでログにタイムスタンプが付けられているのかを理解するのが難しい…。

以下は「stack.sh」の該当箇所のみの引用ですが、スクリプト本体にはコメントがないので、上記のサイトの解説から引っ張ってコメントにしています。

#Copy stdout to fd 3
    exec 3>&1
    if [[ "$VERBOSE" == "True" ]]; then

#Redirect stdout/stderr to tee to write the log file
        exec 1> >( awk '
                {
                    cmd ="date +\"%Y-%m-%d %H:%M:%S \""
                    cmd | getline now
                    close("date +\"%Y-%m-%d %H:%M:%S \"")
                    sub(/^/, now)
                    print
                    fflush()
                }' | tee "${LOGFILE}" ) 2>&1

#Set up a second fd for output
        exec 6> >( tee "${SUMFILE}" )
    else

#Set fd 1 and 2 to primary logfile
        exec 1> "${LOGFILE}" 2>&1

#Set fd 6 to summary logfile and stdout
        exec 6> >( tee "${SUMFILE}" /dev/fd/3 )
    fi

<さらに追記 8/18>

ブコメで以下のコメントをもらったので、ここで出しているwhileループの例について補足します。

whileループのところ、直接 cat inputfile | grep -v exclude | while read line ~~ すればいいのでは?

僕が出していた例は、確かにコメントでいただいたものでも結果は変わりません。ただ、もともとのお題がパイプに伴う変数の扱いの問題で、コメントでいただいたようにパイプでつないだ場合、「ループ内で変数を変更してもそれがメインシェルの変数に反映されない」*4という問題に引っかかります。なので、ここではパイプを使わない場合の例を書きました。

たとえば、以下のスクリプトを実行してみます。

#!/bin/bash
var="first"

cat inputfile | grep -v exclude | while read line
do
    # ループの中で変数書き換え
    var="second"
done
echo $var # whileループ内での変更が反映されて、"second"が出るはず・・・?

while read line
do
    # ループの中で変数書き換え
    var="second"
done < <(cat inputfile | grep -v exclude)

echo $var # 上と同様の結果???

2つのループの違いは、パイプかプロセス置換での擬似的なファイル入力かだけですが、こちらの出力結果は、以下のようになります。

first
second

前者の場合は、var="second"への変更がWhile内部だけでしか有効にならないため、その外にあるvar変数に反映されません。一方、後者ではwhileループの中で変数変更が反映されています。*5この辺りはお題のエントリの解答編に詳しく書かれているので、そちらをご参照ください。

*1:プロセス置換は、zshでも使えるようですが、僕自身はzsh使いではないので、ここでは触れません

*2:プロセス置換を使わなくてもファイルに標準出力と標準エラー出力をリダイレクトして、それをtailすることで実現できますが、そうすると別途tailのプロセスの扱いを考えたりする必要があって手間がかかります。

*3:これがhirose31さんの日記にコメントしたものです

*4:この原因は、サブシェルの起動によるものだと説明されることが多く、僕もこれまでそう思っていましたが、お題のブログへのコメントに「bash (や POSIX sh) は、コマンドパイプラインの変数値変更はそのコマンドラインローカルになるから」ということではないかということが書かれていました。自分の手元で実験した限りでは、確かに指摘の通りで、サブシェルを使わない場合でも同じ現象となりました。

*5:一時ファイルを使った場合もプロセス置換と同様の結果になります。