はじめに
シェルスクリプトのエラー処理は難しい場面がいくつかあります。そのため信頼性の高いシェルスクリプトを書くのが困難になっています。POSIX.1-2024 での set -o pipefail の標準化でパイプラインのエラー処理を移植性がある形で実装することができるようになりました。しかしまだエラー処理が難しい場面が残っています。それがプロセス置換です。この記事ではプロセス置換のエラー処理を行う方法について解説します。ちなみにこの記事の対象シェルはプロセス置換が実装されているbash、ksh93、zshです。
なぜプロセス置換は終了ステータスを取れない?
プロセス置換の書き方はこのようなものです。
$ diff -u <(echo foo) <(echo bar)
--- /dev/fd/63  2024-07-15 08:12:04.455507170 +0900
+++ /dev/fd/62  2024-07-15 08:12:04.459507190 +0900
@@ -1 +1 @@
-foo
+bar
ここでプロセス置換で実行しているコマンドでエラーを発生させ、終了ステータスを見てみましょう。
$ diff -u <(seq -f '%x' 1) <(seq -f '%y' 1)
seq: format ‘%x’ has unknown %x directive
seq: format ‘%y’ has unknown %y directive
$ echo $?
0
ご覧の通り、エラーが発生しているのに終了ステータスは正常 (0)です。当然ですがこれはパイプラインではないので PIPESTATUS は役に立ちません。
なぜ終了ステータスが取れないかと言うと、プロセス置換の中のコマンドは呼び出し元プロセスとは独立して非同期で実行されているからです。終了ステータスが取れないのもあたりまえですね。
さてこの問題、どうやって解決しましょうか?
サブシェルの中なら終了ステータスを取れる
コードが複雑になるのでここからはシェルスクリプトで説明します。終了ステータスが取れないと言っても、それは呼び出し元プロセスの話で、プロセス置換のサブシェルの中なら当然取ることができます。
#!/bin/bash
diff -u <(seq -f '%x' 1; echo "ex1=$?" >&2) <(seq -f '%y' 1; echo "ex2=$?" >&2)
$ ./script.sh
seq: format ‘%x’ has unknown %x directive
seq: format ‘%y’ has unknown %y directive
ex2=1
ex1=1
ここまでわかれば後は簡単ですね? いつものやつを書くだけです。ですよね?
完成
ぱっと書けるか?と言われたら今でも考えながらでないと書けないわけですが、さすがにこんな感じでやればできるよね?ぐらいにまでは慣れました。不要なファイルディスクリプタ、ちゃんと閉じれてるよね?
#!/bin/bash
{
  xs=$(
    {
      diff -u \
        <(seq -f '%x' 1 4>&-; echo "xs1=$?" >&4) \
        <(seq -f '%y' 1 4>&-; echo "xs2=$?" >&4) >&3 3>&-
    } 4>&1
  )
  eval "xs=$? $xs"
} 3>&1
echo "xs: $xs, xs1: $xs1, xs2: $xs2"
$ ./script.sh
seq: format ‘%x’ has unknown %x directive
seq: format ‘%y’ has unknown %y directive
xs: 0, xs1: 1, xs2: 1
もう少し真面目にやるなら set -e の処理を付け加えるのですが、書かなくても大丈夫ですよね? できるのはわかってるんですが、真面目に書くと面倒だなーと。
さいごに
ということで、私の中ではおなじみのパターンになっちゃっているわけですが、よくよく考えたら誰もこのやり方知らないんじゃね?と思って書いてみました。ちゃんと調べてないので、すでにどこかで誰かが紹介しているかもしれませんが。ちょっと込み入った話になると真面目にシェルスクリプトのエラー処理書くのって面倒ですよねぇ。
あとは発展として、これを使いやすいライブラリの形に落とし込めたらなーと思っています。うーん、どういうインターフェースにすればいいだろうか、プロセス置換の数は2個とは限らないし、名前付きパイプを使って POSIX で標準化された機能だけで実現したいし。まあ今すぐ必要なわけではないのでそのうち。