コマンドの出力をファイル名で受け取るコマンドに渡したい
diffやcommのようにファイル名を引数に取るコマンドへ、別のコマンドの出力を渡したい場面がある。素直に書くと一時ファイルを経由する。
$ sort a.txt > /tmp/a.sorted
$ sort b.txt > /tmp/b.sorted
$ diff /tmp/a.sorted /tmp/b.sorted
$ rm /tmp/a.sorted /tmp/b.sorted
bashやzshのプロセス置換<(command)を使うと、一時ファイルなしで1行にまとまる。
$ diff <(sort a.txt) <(sort b.txt)
プロセス置換の仕組み
<(command)はコマンドをバックグラウンドで実行する。展開結果は、そのコマンドの標準出力を読み出せるパス名である。bashでの展開結果をechoで確認する。
$ echo <(true)
/dev/fd/63
/dev/fd/63はシェルが用意したパイプであり、実体はファイルではない。statで種別を見るとFIFOと表示される。
$ stat -L -c "%s %F" <(seq 1 1000)
0 fifo
つまりプロセス置換は、コマンド出力をファイル名の形で他のコマンドへ手渡す仕組みである。bashでは複数書くと、ファイルディスクリプタ番号が63から降順に割り当てられる。
$ echo <(true) <(true) <(true)
/dev/fd/63 /dev/fd/62 /dev/fd/61
展開されるパス名はシェルや環境によって異なる。Linuxのzshは/proc/self/fd/11のようなパスを使う。/dev/fdが使えない環境では、シェルが一時ディレクトリに名前付きパイプ(FIFO)を作って代用する。
似た記法のコマンド置換$(command)は、コマンドの出力そのものへ展開される点が異なる。
| 記法 | 展開結果 |
|---|---|
$(command) | コマンドの出力文字列 |
<(command) | コマンドの出力を読み出せるパス名 |
標準入力へ渡す < <(command)
引数としてではなく標準入力へ渡す場合は、リダイレクトと組み合わせて< <(command)と書く。<と<(の間の空白は必須で、詰めて書くと構文エラーになる。
$ wc -l < <(seq 1 4)
4
$ wc -l <(seq 1 4)
4 /dev/fd/63
引数として渡した場合、wcはファイル名も一緒に表示する。標準入力から読ませたい場合はリダイレクト形式を使う。
パイプのサブシェル問題を避ける
command | while readの形ではループがサブシェルで実行されるため、ループ内で更新した変数がループを抜けると失われる。
$ count=0
$ seq 1 3 | while read -r line; do count=$((count+1)); done
$ echo "$count"
0
< <(command)で標準入力から読ませれば、whileループは現在のシェルで実行される。変数の更新も残る。
$ count=0
$ while read -r line; do count=$((count+1)); done < <(seq 1 3)
$ echo "$count"
3
bashにはshopt -s lastpipeでパイプ最終要素を現在のシェルで実行する設定もある。ただしジョブ制御が有効な間は働かないため、対話シェルでは効果がない。
出力プロセス置換 >(command)
>(command)は逆向きに働く。展開されたパス名へ書き込んだ内容が、コマンドの標準入力へ流れる。teeと組み合わせると、出力を複数の処理へ分岐できる。
$ seq 1 5 | tee >(wc -l > count.txt) > /dev/null
$ cat count.txt
5
標準エラー出力だけを加工したい場合にも使える。
$ ls /nonexistent 2> >(sed "s/^/ERR: /" >&2)
ERR: ls: cannot access '/nonexistent': No such file or directory
>(command)の中のコマンドは非同期に実行されるため、出力の到着順は保証されない。後続の出力より遅れて表示される場合がある。
具体例
共通行・差分行を抽出する
commはソート済みファイルを2つ受け取る。プロセス置換の中でソートまで済ませられる。
$ comm -12 <(sort a.txt) <(sort b.txt)
別コマンドの出力をパターンファイルにする
grep -fはパターンを書いたファイルを受け取る。
$ grep -F -f <(cut -d, -f1 target.csv) access.log
2つのコマンド出力を結合する
joinやpasteのような複数ファイルを取るコマンドとも相性がよい。
$ join <(printf '1 alice\n2 bob\n') <(printf '1 tokyo\n2 osaka\n')
1 alice tokyo
2 bob osaka
ソート済みデータをマージする
$ sort -n -m <(seq 1 2 9) <(seq 2 2 10)
awkで2つのコマンド出力を突き合わせる
$ awk 'NR==FNR{a[$0];next} $0 in a' <(seq 1 5) <(seq 3 8)
3
4
5
注意点
POSIXシェルでは使えない
プロセス置換はbash、zsh、kshの拡張であり、POSIXの仕様には含まれない。Debian系で/bin/shの実体となっているdashで実行すると構文エラーになる。
$ dash -c 'diff <(echo a) <(echo b)'
dash: 1: Syntax error: "(" unexpected
シェルスクリプトで使う場合は、shebangを#!/bin/bashにする。#!/bin/shのままbash script.shのように実行できても、cronやDockerのENTRYPOINTなどshで起動される経路が残っていると失敗する。
終了ステータスが外側へ伝わらない
プロセス置換の中のコマンドが失敗しても、外側のコマンドの終了ステータスには反映されない。
$ cat <(exit 3)
$ echo "$?"
0
set -eやset -o pipefailでも検知できない。bash 4.4以降は$!にプロセス置換のプロセスIDが入るため、waitで終了ステータスを拾える。
$ cat <(exit 3)
$ wait "$!"
$ echo "$?"
3
ただしmacOS標準のbash 3.2やzshでは$!にプロセスIDが入らない。移植性が必要な場合は、失敗を検知したい処理をプロセス置換の外に出す。
シークが必要なコマンドには渡せない
プロセス置換の実体はパイプであり、先頭へ戻る読み方に対応しない。ZIPファイルのように末尾の情報を先に読む形式は扱えない。
$ unzip -l <(cat archive.zip)
Archive: /dev/fd/63
End-of-central-directory signature not found. Either this file is not
a zipfile, or it constitutes one disk of a multi-part archive. In the
latter case the central directory and zipfile comment will be found on
the last disk(s) of this archive.
unzip: cannot find zipfile directory in one of /dev/fd/63 or
/dev/fd/63.zip, and cannot find /dev/fd/63.ZIP, period.
先頭から順に読むコマンドであれば問題ない。シークが必要な場合は一時ファイルを作る。zshには一時ファイルを経由する=(command)があり、シークが必要なコマンドへも渡せる。
$ zsh -c 'echo =(true)'
/tmp/zshXORuYz
$ zsh -c 'unzip -l =(cat archive.zip)'
Archive: /tmp/zshRTLj7q
Length Date Time Name
--------- ---------- ----- ----
6 08-10-2026 23:32 f.txt
--------- -------
6 1 file
>(command) の完了を待たない
出力プロセス置換の中のコマンドは非同期に実行される。処理に時間がかかると、直後に結果を参照しても間に合わない。
$ seq 1 5 | tee >(sleep 1; wc -l > slow.txt) > /dev/null
$ cat slow.txt
cat: slow.txt: No such file or directory
$ sleep 2
$ cat slow.txt
5
結果を後続の処理で使う場合は、素直にパイプラインでつなぐか、書き込み完了を確認してから読む。
変数へ代入しても後で使えない
代入時点でパス名へ展開されるが、代入したコマンドが終わるとシェルはファイルディスクリプタを閉じる。後から読み出そうとしても失敗する。
$ f=<(seq 1 3)
$ echo "$f"
/dev/fd/63
$ cat "$f"
cat: /dev/fd/63: No such file or directory
/dev/fd/63はプロセスごとのファイルディスクリプタを指すため、そもそも別のプロセスから開き直せない。プロセス置換は、利用するコマンドの引数として直接書く。
参考
- Bash Reference Manual - Process Substitution
- コマンド結果をdiffコマンドで比較する
- 【comm】ソート済みファイル同士を比較して差分・共通部分を抽出する
- 【Shell Script】PIPESTATUSでパイプ途中のコマンドの終了コードを取得する
