シェルスクリプトのcoproc設計|bash組み込みの双方向パイプで対話プロセスへの繰り返し入力を実装する方法

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのcoproc設計|bash組み込みの双方向パイプで対話プロセスへの繰り返し入力を実装する方法
「スクリプトからbcやsqlite3に繰り返し値を送りたいのに、パイプで書くとその都度プロセスが起動し直してしまう」
mkfifo(名前付きパイプ)を使う方法もありますが、FIFOファイルの手動管理が必要で、スクリプトが複雑になりがちです。

この記事では、bashのcoproc(コプロセス)コマンドを使った双方向通信設計を解説します。coprocはbashの組み込み機能で、コプロセスとの読み書き用ファイルディスクリプタ(FD)を自動的に作成・管理するため、mkfifoより簡潔な構文で永続プロセスへの繰り返し入力が実現できます。RHEL 9系/Ubuntu 24.04 LTSを想定環境として説明します。

この記事のポイント

・ coproc NAMEでbashが双方向パイプFDを自動管理し、外部コマンドが不要
・ ${NAME[0]}が読み取りFD、${NAME[1]}が書き込みFDとして使える
・ 永続プロセスへの繰り返し入力でbcやsqlite3の起動コストを削減できる
・ バッファリング問題はstdbuf -oLで解消できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜcoproc(コプロセス)が必要なのか

シェルスクリプトから外部プログラムへ値を繰り返し送る場面を考えます。たとえば、100件の浮動小数点計算をbcで実行する場合、次のような書き方だとループのたびにbcが起動・終了します。

# 毎回bcを起動し直す書き方 for i in $(seq 1 100); do result=$(echo "$i * 3.14159" | bc -l) echo "$i * pi = $result" done

100件の計算でbcを100回起動・終了するため、起動コストが積み重なります。bcは起動が速いので影響が出にくいですが、JVMやPythonインタープリタを呼び出すツールでは無視できない遅延になります。

この問題の解決策は3つあります。
手法 特徴 難易度
パイプで都度起動 シンプルだが毎回プロセス起動 低
mkfifoで名前付きパイプ FIFOファイルの手動管理が必要 中
coproc(コプロセス) bash組み込みでFD管理を自動化 中
coprocはbash 4.0以降で使えるbashの組み込み機能です。外部コマンドなしにコプロセスとの双方向パイプを確立するため、mkfifoよりシンプルなコードで永続プロセス通信を実装できます。

coprocの基本構文と動作

1. 名前付きコプロセスの宣言

coprocの基本構文は次のとおりです。

# 複合コマンドとして宣言する書き方(推奨) coproc NAME { command; } # 単純なコマンドの場合 coproc NAME command

coprocを宣言すると、bashは次の変数を自動的に設定します。

・ ${NAME[0]}:コプロセスの標準出力からの読み取りFD
・ ${NAME[1]}:コプロセスの標準入力への書き込みFD
・ $NAME_PID:コプロセスのプロセスID

コプロセスはバックグラウンドで動作し続けます。スクリプトがFDを通じて値をやり取りする間、コプロセスは常に起動したままです。

2. FDを使った読み書きの基本

coprocのFDを使う基本パターンを示します。

# コプロセスを起動(-l は数学ライブラリを有効化) coproc BC { bc -l; } # 式を書き込む: コプロセスの標準入力へリダイレクト printf '10 * 20\n' >&${BC[1]} # 結果を読み取る: コプロセスの標準出力からリダイレクト read result <&${BC[0]} echo "10 * 20 = $result" printf 'sqrt(2)\n' >&${BC[1]} read result <&${BC[0]} echo "sqrt(2) = $result"

出力例(環境により異なります):

10 * 20 = 200 sqrt(2) = 1.41421356237309504880

>&${BC[1]} で書き込みFDへリダイレクトし、<&${BC[0]} で読み取りFDからリダイレクトします。bashがFD番号を管理するため、実際の数値(3や4など)は意識しなくてよい点もcoprocの利点です。

3. コプロセスの終了処理

コプロセスを終了するには、書き込みFDを閉じてからwaitで回収します。

# 書き込みFDを閉じる(コプロセスのstdinがEOFになり終了する) exec {BC[1]}>&- # コプロセスの終了を待つ wait $BC_PID

FDを閉じずにスクリプトが終了すると、コプロセスが残留することがあります。trap EXITでの後片付けに組み込むと確実です。

実装パターン|bcを使った繰り返し算術計算

coprocを使った永続bcセッションの実装例を示します。ループを通じて同じbcセッションを維持することで、100件の計算をbcの起動1回で処理できます。

#!/bin/bash # 後片付けを確実にするtrapを設定 cleanup() { exec {BC[1]}>&- 2>/dev/null wait "$BC_PID" 2>/dev/null } trap cleanup EXIT # 永続bcセッションをcoprocで起動 coproc BC { bc -l; } echo "--- 1つのbcセッションで繰り返し計算 ---" for angle in 0 30 45 60 90; do # degree を radian に変換してsin値を計算 printf 's(%s * 3.14159265358979 / 180)\n' "$angle" >&${BC[1]} read sin_val <&${BC[0]} printf "sin(%3d度) = %s\n" "$angle" "$sin_val" done

出力例(環境により異なります):

--- 1つのbcセッションで繰り返し計算 --- sin( 0度) = 0 sin( 30度) = .49999999999999999991 sin( 45度) = .70710678118654752440 sin( 60度) = .86602540378443864676 sin( 90度) = 1.00000000000000000000

bc -l のs()関数(sin)を使っています。ループを通じて同じbcインスタンスが継続するため、数学ライブラリの状態も保たれます。

シェルスクリプトの実装設計をより体系的に学びたい方は、シェルスクリプト実践講座もあわせて参考にしてみてください。

バッファリング問題とstdbuf

なぜバッファリングが問題になるのか

bcは行バッファリングで動作するため、先ほどの例のようにreadが正常に結果を受け取れます。しかし、sqlite3など多くのプログラムはパイプに接続されるとブロックバッファリングに切り替わります。ブロックバッファリングでは出力がバッファいっぱいになるまでフラッシュされないため、readがいつまでも応答を受け取れずにハングします。

stdbuf -oL で行バッファリングに変更する

stdbuf -oL でコプロセスを起動すると、標準出力を行バッファリングに設定できます。

# stdbuf -oL でsqlite3の標準出力を行バッファリングに設定 coproc SQL { stdbuf -oL sqlite3 "$DB_PATH"; } # クエリを送信してデリミタトークンを追加 printf 'SELECT name FROM users LIMIT 3; SELECT "END_OF_RESULTS";\n' >&${SQL[1]} # "END_OF_RESULTS"が来るまで結果を読み取る while IFS= read -r line <&${SQL[0]}; do [[ "$line" == "END_OF_RESULTS" ]] && break echo "row: $line" done

各行の末尾の改行で即座にフラッシュされるため、readが確実に応答を受け取れます。プロセス置換(process substitution)は一方向の接続ですが、coprocを使うと双方向の永続セッションが実現できます。

stdbufが使えない環境では、unbuffer(expectパッケージ)が代替として使えます。

トラブルシュート

1. readでハングする場合

readが応答を待ち続けてハングする場合、コプロセスの出力がバッファリングされています。

・ 対処1:stdbuf -oL でコプロセスを起動してバッファをフラッシュさせる
・ 対処2:read -t 秒数 でreadにタイムアウトを設定して強制抜け出しを入れる
・ 対処3:コプロセス側のコマンドが改行で終わる出力を出しているかを確認する

2. コプロセスが意図せず終了する場合

コプロセスが途中で終了すると、書き込みFDへの送信でエラーが発生します。

・ 確認方法:kill -0 $BC_PID 2>/dev/null で生死を確認する(終了コード0が起動中)
・ 主な原因:コプロセスへの不正な入力(bcなら未対応の構文)、パイプのFDがすでに閉じられている
・ 対処:コプロセスの起動を関数化し、エラー時に再起動できる構造にする

3. bash 4.0未満では使えない

coprocはbash 4.0以降の機能です。#!/bin/bash と明示してください。macOSの標準bashはbash 3.2のため、HomebrewでBash 5系をインストールする必要があります。シェルのバージョンは bash --version で確認できます。

本記事のまとめ

bashのcoproc(コプロセス)を使った双方向通信設計のポイントをまとめます。
やりたいこと coprocの書き方
コプロセスを起動する coproc NAME { cmd; }
コプロセスへ書き込む printf '...\n' >&${NAME[1]}
コプロセスから読み取る read var <&${NAME[0]}
コプロセスのPIDを取得する $NAME_PID
コプロセスを終了させる exec {NAME[1]}>&-; wait $NAME_PID
バッファリング問題を解消する coproc NAME { stdbuf -oL cmd; }
・ coprocはbash 4.0以降の組み込み機能で、外部コマンドなしに双方向FDを管理できる
・ mkfifoと比べてFIFOファイルの手動管理が不要で、スクリプトがシンプルになる
・ 永続プロセスへの繰り返し入力で起動コストを削減でき、bcやsqlite3の活用に有効
・ バッファリング問題はstdbuf -oLで対応でき、readのハングを防げる
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、シェルスクリプト実践講座はこちら >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。