web-dev-qa-db-ja.com

bash:プロセス置換のエラーを伝播するにはどうすればよいですか?

シェルスクリプトを使用して実行されたコマンドが失敗するたびに失敗するようにしたい。

通常私はそれを次のように行います:

set -e
set -o pipefail

(通常、私はset -uも)

問題は、上記のいずれもプロセス置換では機能しないことです。このコードは "ok"を出力し、戻りコード= 0で終了しますが、失敗させたいです。

#!/bin/bash -e
set -o pipefail
cat <(false) <(echo ok)

「pipefail」に相当するものはありますか?コマンドの出力をファイルとしてそのままコマンドに渡す他の方法はありますが、それらのプログラムのいずれかが失敗するたびにエラーが発生しますか?

貧しい人々の解決策は、それらのコマンドがstderrに書き込むかどうかを検出することです(ただし、一部のコマンドは、成功したシナリオではstderrに書き込みます)。

もう1つのよりposix準拠のソリューションは名前付きパイプを使用することですが、コンパイルされたコードからオンザフライで構築されたワンライナーとしてそれらのコマンドを使用するプロセス置換を起動する必要があり、名前付きパイプを作成すると事態が複雑になります(追加のコマンド、エラーのトラップ)それらを削除するなど)

19
juanleon

たとえば、次のような場合にのみ、この問題を回避できます。

_cat <(false || kill $$) <(echo ok)
other_command
_

スクリプトのサブシェルはSIGTERMdで、2番目のコマンドを実行できます(_other_command_)。 _echo ok_コマンドが「ときどき」実行される:問題は、プロセス置換が非同期であることです。 _kill $$_コマンドが実行される保証はありませんbeforeまたはafter_echo ok_コマンド。これは、オペレーティングシステムのスケジューリングの問題です。

次のようなbashスクリプトについて考えてみます。

_#!/bin/bash
set -e
set -o pipefail
cat <(echo pre) <(false || kill $$) <(echo post)
echo "you will never see this"
_

このスクリプトの出力は次のようになります。

_$ ./script
Terminated
$ echo $?
143           # it's 128 + 15 (signal number of SIGTERM)
_

または:

_$ ./script
Terminated
$ pre
post

$ echo $?
143
_

あなたはそれを試すことができ、数回の試行の後、出力に2つの異なる注文が表示されます。最初のスクリプトでは、他の2つのechoコマンドがファイル記述子に書き込む前にスクリプトが終了しました。 2番目のコマンドでは、falseコマンドまたはkillコマンドがechoコマンドの後にスケジュールされた可能性があります。

または、より正確に言うと、killシグナルをシェルプロセスに送信するSIGTERMユーティリティのシステムコールsignal()がスケジュールされた(または配信された)のが遅いか早いecho write() syscallsよりも。

ただし、スクリプトは停止し、終了コードは0ではありません。したがって、問題は解決するはずです。

別の解決策はもちろん、これに名前付きパイプを使用することです。ただし、名前付きパイプや上記の回避策を実装するのがどれほど複雑かは、スクリプトによって異なります。

参考文献:

6
chaos

この例は、killtrapと一緒に使用する方法を示しています。

#! /bin/bash
failure ()
{
  echo 'sub process failed' >&2
  exit 1
}
trap failure SIGUSR1
cat < <( false || kill -SIGUSR1 $$ )

ただし、killは、サブプロセスから親プロセスに戻りコードを渡すことができません。

3
ceving

参考までに、回答やコメントが適切で参考になったとしても、私は少し異なるものの実装を終了しました(親プロセスでのシグナルの受信に関して、質問で言及しなかったいくつかの制限がありました)。

基本的に、私はこのようなことをやめました:

command <(subcomand 2>error_file && rm error_file) <(....) ...

次に、エラーファイルを確認します。存在する場合、どのサブコマンドが失敗したかがわかります(error_fileの内容が役立つ場合があります)。私が最初に欲しかったより冗長でハックですが、ワンライナーbashコマンドで名前付きパイプを作成するよりも面倒ではありません。

3
juanleon

同じように implement $PIPESTATUS/$pipestatusをサポートしていないPOSIXシェルを使用する場合 、パイプを介してコマンドを渡すことで、コマンドの終了ステータスを取得できます。

unset -v false_status echo_status
{ code=$(
    exec 3>&1 >&4 4>&-
    cat 3>&- <(false 3>&-; echo >&3 "false_status=$?") \
             <(echo ok 3>&-; echo >&3 "echo_status=$?")
);} 4>&1
cat_status=$?
eval "$code"
printf '%s_code=%d\n' cat   "$cat_status" \
                      false "$false_status" \
                      echo  "$echo_status"

それは与える:

ok
cat_code=0
false_code=1
echo_code=0

または、pipefailを使用して、それをサポートしないシェルの場合と同様に、手動でプロセス置換を実装することもできます。

set -o pipefail
{
  false <&5 5<&- | {
    echo OK <&5 5<&- | {
      cat /dev/fd/3 /dev/fd/4
    } 4<&0 <&5 5<&-
  } 3<&0
} 5<&0
1