2026年8月1日
2026年8月1日
bashのプロセス置換(<()と>())の使い方|diffでの比較・while readの変数消失対策・ログ分岐
bashのプロセス置換は、コマンドの出力(または入力)を「ファイル名」として扱えるようにする機能です。diff <(sort a.txt) <(sort b.txt) のように一時ファイルを作らずに2つのコマンド結果を比較したり、while read ループで変数がループの外で消える問題を避けたりするときに使います。この記事では、<() と >() の仕組み、代表的な使い方、パイプとの違い、そして「終了ステータスが取れない」「sh で動かない」といったつまずきやすい点を説明します。
(2026年10月現在)主要な Linux ディストリビューションの bash は 5 系です。プロセス置換は bash、zsh、ksh の機能で、POSIX sh の仕様には含まれていません。
プロセス置換の仕組み
<(コマンド) と書くと、bash はコマンドをバックグラウンドで起動し、その出力につながったパイプを /dev/fd/63 のようなファイル名に置き換えます。実際に何に置き換わるかは echo で確認できます。
$ echo <(true)
/dev/fd/63
つまり diff <(sort a.txt) <(sort b.txt) は、diff から見ると diff /dev/fd/63 /dev/fd/62 のように2つのファイル名を受け取っているのと同じです。ファイル名を引数に取るコマンドなら、標準入力に対応していなくても使えるのがプロセス置換の利点です。/dev/fd が使えない環境では、名前付きパイプ(FIFO)が代わりに使われます。
>(コマンド) はその逆で、書き込み先として使えるファイル名に置き換わり、そこに書かれた内容がコマンドの標準入力に渡されます。
どちらも < や > と ( の間に空白を入れてはいけません。< (sort a.txt) と書くと、リダイレクトとサブシェルとして解釈され、構文エラーになります。
使い方1:2つのコマンド結果を diff で比較する
最もよく使われるのが比較です。一時ファイルを作って比較し、最後に消す、という手順が1行で済みます。
# 並び順を揃えてから比較する
diff <(sort list_a.txt) <(sort list_b.txt)
# 片方にだけある行を表示する(comm は入力がソート済みである必要がある)
comm -13 <(sort old.txt) <(sort new.txt) # new.txt にだけある行
サーバー運用では、ローカルとリモートの設定ファイルの比較にも使えます。
diff <(ssh web01 cat /etc/nginx/nginx.conf) /etc/nginx/nginx.conf
WordPress を複数環境で運用している場合、WP-CLI と組み合わせて本番とステージングで有効なプラグインの差を確認できます。
diff \
<(wp plugin list --status=active --field=name --path=/var/www/staging | sort) \
<(wp plugin list --status=active --field=name --path=/var/www/production | sort)
どちらも読み取りだけの操作なので、本番環境で実行しても内容は変わりません。
使い方2:while read で変数が消える問題を避ける
パイプでループに渡すと、ループの中で変更した変数がループを抜けたあとに元に戻ってしまいます。
count=0
grep ' 500 ' access.log | while read -r line; do
count=$(( count + 1 ))
done
echo "$count" # 0 のまま
bash ではパイプの各段が別プロセス(サブシェル)で実行されるため、ループ内の count は親シェルの count とは別物です。プロセス置換を使って入力を渡せば、ループは現在のシェルで実行されます。
count=0
while read -r line; do
count=$(( count + 1 ))
done < <(grep ' 500 ' access.log)
echo "$count" # 実際の件数
done < <(...) の最初の < は通常の入力リダイレクト、続く <(...) がプロセス置換です。2つの < の間には空白が必要です。
コマンドの出力を配列に読み込む場合も同じ考え方です(次の例の wp site list はマルチサイト用のコマンドです)。
mapfile -t sites < <(wp site list --field=url)
echo "サイト数: ${#sites[@]}"
なお、スクリプト内(ジョブ制御が無効な状態)なら shopt -s lastpipe を設定すると、パイプの最後のコマンドが現在のシェルで実行されるようになり、パイプのままでも変数が残ります。どちらを使うかはチームの読みやすさで選んで構いません。
使い方3:>() で出力を分岐させる
tee と >() を組み合わせると、1つの出力を複数の処理に同時に流せます。
# 圧縮したダンプを保存しながら、同じデータのチェックサムも計算する
# (接続情報は ~/.my.cnf に置いている前提)
mysqldump --single-transaction wordpress \
| gzip \
| tee >(sha256sum > backup.sql.gz.sha256) > backup.sql.gz
データを1回流すだけで保存とチェックサム計算が済むため、大きなダンプでも読み直しが不要です。ただし後述のとおり >() の処理は非同期に終わり、しかもパイプラインの途中で使った >() は後で紹介する wait $! で待つこともできません。直後の行でチェックサムファイルを使う処理を続けるなら、この書き方は避けて保存後に計算してください。ただし sha256sum は標準入力から読んでいるので、出力されるファイル名の欄は - になります。sha256sum -c で検証に使う場合はファイル名を書き換えるか、保存後のファイルに対して sha256sum backup.sql.gz を実行してください。
分岐は、標準エラー出力だけを別のコマンドで加工したいときにも便利です。
# 標準エラー出力に日時を付けてファイルに追記する
./backup.sh 2> >(while read -r line; do
printf '%s %s\n' "$(date '+%F %T')" "$line"
done >> backup_error.log)
スクリプト全体の出力を画面とログファイルの両方に出す定番の書き方もあります。
#!/usr/bin/env bash
exec > >(tee -a /var/log/myjob.log) 2>&1
echo "ここから先の出力はログにも残る"
つまずきやすい点
sh で実行するとエラーになる
Debian や Ubuntu では /bin/sh が dash で、dash はプロセス置換に対応していません。sh script.sh と実行したり、シバンが #!/bin/sh だったりすると、Syntax error: "(" unexpected のようなエラーになります。プロセス置換を使うスクリプトはシバンを #!/usr/bin/env bash(または #!/bin/bash)にし、bash script.sh か実行権限を付けて ./script.sh で実行してください。cron から呼ぶ場合も、crontab の SHELL の既定は /bin/sh なので、コマンドを直接書くのではなくスクリプトファイルにまとめると安全です。
中のコマンドの終了ステータスが取れない
プロセス置換の中のコマンドが失敗しても、$? や set -e、pipefail には反映されません。
while read -r line; do
echo "$line"
done < <(grep pattern missing_file.txt)
echo $? # grep が失敗しても 0
bash 4.4 以降では、直前のプロセス置換のプロセスID が $! に入り、wait $! でその終了ステータスを取得できます。
while read -r line; do
echo "$line"
done < <(grep pattern data.txt)
wait $! || echo "grep が失敗または一致なし(終了ステータス $?)" >&2
grep は一致がないときも終了ステータス1を返すので、「エラー」と「0件」を区別したい場合は、終了ステータスが2以上かどうかで判定します。失敗の検出が重要な処理(バックアップやデプロイ)では、一度ファイルに書き出してから終了ステータスを確認する方が見通しのよいこともあります。
>() の処理は非同期に終わる
>() の中のコマンドは、メインのコマンドとは別に動いています。メインのコマンドが終わった時点で、>() 側の書き込みが終わっているとは限りません。スクリプトの直後の行でその出力ファイルを読むと、中身が途中までしかないことがあります。
some_command > >(gzip > out.gz)
wait $! # bash 4.4 以降。gzip の完了を待つ
ls -l out.gz
wait $! で待てるのは、現在のシェルで実行したコマンドに付けたプロセス置換だけです。a | tee >(b) | c のようにパイプラインの途中で使うと、プロセス置換を作るのはパイプの各段のサブシェルなので、親シェルの $! には入りません。対話シェルで >() を使うと、プロンプトが表示されたあとに出力が出てくるのも、非同期に動いているためです。
ファイルとして読めるのは1回だけ
プロセス置換が渡すのはパイプなので、先頭に戻って読み直すこと(シーク)はできません。ファイルを2回読むコマンドや、ファイルサイズを先に調べるコマンドでは、期待どおりに動かないことがあります。その場合は一時ファイルを使います。
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
sort list_a.txt > "$tmp"
パイプ・コマンド置換との使い分け
- パイプ
a | b:1つの出力を1つのコマンドに渡す、最も基本的な方法 - コマンド置換
$(a):出力を文字列として変数や引数に埋め込む。大きな出力には向かない - プロセス置換
<(a):出力をファイル名として渡す。ファイル名を2つ以上取るコマンドや、現在のシェルで読み込みたいときに使う
パイプで書けるところはパイプで書き、「ファイル名が必要」「変数を残したい」「出力を分岐したい」場面でプロセス置換を選ぶと、読みやすいスクリプトになります。
まとめ
<(コマンド)は出力を、>(コマンド)は入力を、/dev/fd/63のようなファイル名として渡すdiff <(sort a) <(sort b)で、一時ファイルなしに2つの結果を比較できるwhile read ...; done < <(コマンド)ならループ内で変更した変数が残る- dash など POSIX sh では使えないため、シバンを bash にして bash で実行する
- 中のコマンドの終了ステータスは
$?に反映されない。bash 4.4 以降はwait $!で取得できる >()は非同期に終わるため、直後に結果を使うなら完了を待つ