はじめに
bash は POSIX準拠のシェルですが、加えて独自の機能拡張もなされています。
これまでは汎用性の面から拡張機能の利用は敬遠がちだった私ですが、いざ使い始めると便利で、いつの間にか手に馴染んでしまった機能が幾つかあります。
以前の記事では、その中から中括弧による繰り返し展開 を紹介しました。
今回は プロセス置換 を紹介したいと思います。
記事タイトルをbash限定としていますが、この機能はzshなどにもあります。
プロセス置換
元の英語 "Process Substitution" から来ているのだと思いますが、プロセス置換 という言葉だけを聞いて、それがどんな機能なのかピンと来る人は少ないのではないかと思います。
私もそうでした。現在は以下のように理解しています。
プロセス置換
⇒ ファイルをプロセス(コマンド)に置換
⇒ ファイルを記述する替わりに、その位置にプロセス(コマンド)を記述
プロセス置換の書式:
- <(コマンド) :入力ファイルを置換する場合
- >(コマンド) :出力ファイルを置換する場合
使用するモチベーション
実践の場において、結果をdiffで比較したいのだけど、その比較対象のファイルは加工が必要な場合がよくあります。
grep 'hoge' f1.txt >/tmp/t1.txt #比較対象1を加工
cat f2.txt | sed 's/hege/hoge/' >/tmp/t2.txt #比較対象2を加工
diff /tmp/t1.txt /tmp/t2.txt #加工した結果で比較
上記では、加工結果を保持する一時ファイルを2つ作り、比較を行っています。
また、diffコマンドでファイル名にハイフン(-)を指定する方法を使えば、一時ファイルを1つ減らすことも出来ます。
grep 'hoge' f1.txt >/tmp/t1.txt
cat f2.txt | sed 's/hege/hoge/' | diff /tmp/t1.txt -
ファイル名の引数の位置にハイフン(-)を受け付けるコマンドが多くあります。
POSIX準拠シェルの範疇ではこの辺が落としどころだと思いますが、プロセス置換を使えば一時ファイルは一つも作らずに済みます。
diff <(grep 'hoge' f1.txt) <(cat f2.txt | sed 's/hege/hoge/')
一時ファイルを作らずに済むということは、その後始末も不要 (^o^)/
利用例
前述のように、ファイル名のハイフン(-)とパイプの組みあわせで一時ファイル生成を回避できるケースがあるので、私が実際にプロセス置換を使うのはファイル名引数を複数とるコマンドが中心になります。結果、diffがらみで使うことが多いです。
diff <(iconv -f SJIS sjis.txt) <(iconv -f EUCJP euc.txt)
diff <(ls -1 ./dirA) <(ls -1 ./dirB)
入力系の <(コマンド) と比較して 出力系の >(コマンド) は、パイプやリダイレクトで代用できるケースが大半になるため、実際に使いたくなる場面は減りそうです。
cat A.log B.log C.log | tee >(grep 'key1' > k1.log) | grep 'key2' > k2.log
any-program > >(grep 'key1' > k1.log) 2> >(grep 'key2' > k2.log)
また、出力系の >(コマンド) については、コマンド部の実行が非同期(完了を待たない)になる点に注意して使う必要があります。
echo AAA > >(sleep 5 ; cat)
# すぐにプロンプトに戻り、"AAA" が表示されるのはその5秒後
まとめ
何か処理を行うために「一時ファイルが必要かも?」と思ったら、プロセス置換による代替を考えてみたいところです。
上手くいくと、意味のない一時ファイルを作らずに済みます。
その結果、作った一時ファイルの後始末も不要になります。
私の場合は、次のようなときに使っています。
- コマンドの引数または出力先が一時ファイルになりそう
- それがパイプやハイフン(-)を使っても回避できそうにない
番外編
プロセス置換の正体を少しだけ探ってみました。
# コロン(:)は「何もしない」というコマンドです
$ ls -l <(:)
lr-x------ 1 user group 64 Dec 13 10:12 /dev/fd/63 -> pipe:[18800417]
$ ls -l >(:)
l-wx------ 1 user group 64 Dec 13 10:12 /dev/fd/63 -> pipe:[18803423]
$ ls -l /dev/fd
lrwxrwxrwx 1 root root 13 Nov 13 00:11 /dev/fd -> /proc/self/fd
<(コマンド) にはread権限、>(コマンド) にはwrite権限が付与されています。
また、いずれの場合も自プロセスのファイルディスクリプタ63番とパイプを介してコマンドと結びついているようです。
続いて、プロセス置換を複数記述してみます。
# ls の Uオプションは出力結果の自動ソートを抑制するためのもの
$ ls -lU >(:) <(:) <(:)
l-wx------ 1 user group 64 Dec 13 10:16 /dev/fd/63 -> pipe:[18800507]
lr-x------ 1 user group 64 Dec 13 10:16 /dev/fd/62 -> pipe:[18800508]
lr-x------ 1 user group 64 Dec 13 10:16 /dev/fd/61 -> pipe:[18800509]
どうやらファイルディスクリプタ番号は63から降順に使用されていくようです。
ならば、同時に扱えるプロセス置換は最大64程度なのでしょうか?
# ls -lU <(:) <(:) <(:) <(:) ... と100個並べたくないので;
$ cmd='ls -lU' ; for i in {1..100} ; do cmd="$cmd <(:)" ; done ; eval $cmd
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/63 -> pipe:[18807548]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/62 -> pipe:[18807549]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/61 -> pipe:[18807550]
...(省略)...
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/7 -> pipe:[18807604]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/6 -> pipe:[18807605]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/5 -> pipe:[18807606]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/3 -> pipe:[18807607]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/4 -> pipe:[18807608]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/64 -> pipe:[18807609]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/65 -> pipe:[18807610]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/66 -> pipe:[18807611]
...(省略)...
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/100 -> pipe:[18807645]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/101 -> pipe:[18807646]
lr-x------ 1 user group 64 Dec 13 10:22 /dev/fd/102 -> pipe:[18807647]
- 63から降順に使われていき、それでも足りなくなると64から昇順に使っていく
- 既に使用中の 0, 1, 2 は使わない(標準入出力、標準エラー出力)
いくつかの実装系で試しましたが、3 と 4 の出現順はなぜかこの順に。。。
以上です。
最後までご覧いただきありがとうございます。