web-dev-qa-db-ja.com

シェルパイプでエラーコードをキャッチする

現在、次のようなスクリプトを作成しています

./a | ./b | ./c

A、b、またはcのいずれかがエラーコードで終了した場合、エラーメッセージを出力し、不正な出力をパイプ処理する代わりに停止するように変更します。

そのための最も簡単でクリーンな方法は何でしょうか?

88
hugomg

最初のコマンドが成功するまで、2番目のコマンドを実行したくない場合は、おそらく一時ファイルを使用する必要があります。その簡単なバージョンは次のとおりです。

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

「1>&2」リダイレクトも「>&2」と短縮できます。ただし、MKS Shellの古いバージョンは、先行する「1」なしでエラーリダイレクトを誤って処理したため、年齢の信頼性のためにその明確な表記法を使用しました。

何かを中断すると、ファイルがリークします。耐爆弾(多かれ少なかれ)シェルプログラミングは以下を使用します。

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

最初のトラップ行には、「コマンドを実行する」_rm -f $tmp.[12]; exit 1 'シグナル1 SIGHUP、2 SIGINT、3 SIGQUIT、13 SIGPIPE、または15 SIGTERMのいずれかが発生した場合、または0(何らかの理由でシェルが終了した場合)。シェルスクリプトを記述している場合、最後のトラップは、シェル終了トラップである0のトラップのみを削除する必要があります(プロセスはとにかく終了するので、他の信号はそのままにしておくことができます)。

元のパイプラインでは、「a」が終了する前に「c」が「b」からデータを読み取ることは実行可能です-これは通常望ましいことです(たとえば、複数のコアの作業が必要になります)。 「b」が「ソート」フェーズの場合、これは適用されません。「b」は、出力を生成する前にすべての入力を確認する必要があります。

どのコマンドが失敗するかを検出する場合は、次を使用できます。

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

これは単純で対称的です。4パートまたはNパートのパイプラインに拡張するのは簡単です。

'set -e'を使用した簡単な実験は役に立ちませんでした。

19

bashでは、ファイルの先頭でset -eset -o pipefailを使用できます。 3つのスクリプトのいずれかが失敗すると、後続のコマンド./a | ./b | ./cは失敗します。戻りコードは、最初に失敗したスクリプトの戻りコードになります。

pipefailは標準のshでは使用できないことに注意してください。

143
Michel Samia

完全な実行後に${PIPESTATUS[]}配列を確認することもできます。実行する場合:

./a | ./b | ./c

${PIPESTATUS}はパイプ内の各コマンドからのエラーコードの配列になるため、中央のコマンドが失敗した場合、echo ${PIPESTATUS[@]}は次のようなものになります。

0 1 0

そして、このようなものはコマンドの後に実行されます:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

パイプ内のすべてのコマンドが成功したことを確認できます。

39
Imron

残念ながら、Johnathanの回答には一時ファイルが必要で、MichelとImronの回答にはbashが必要です(この質問にはShellというタグが付いていますが)。すでに他の人が指摘したように、後のプロセスが開始される前にパイプを中止することはできません。すべてのプロセスは一度に開始されるため、エラーを通知する前にすべて実行されます。しかし、質問のタイトルはエラーコードについても尋ねていました。これらは、パイプの終了後に取得および調査して、関連するプロセスのいずれかが失敗したかどうかを判断できます。

これは、最後のコンポーネントのエラーだけでなく、パイプ内のすべてのエラーをキャッチするソリューションです。そのため、これはbashのpipefailのようなもので、エラーコードallを取得できるという意味でより強力です。

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

何かが失敗したかどうかを検出するために、コマンドが失敗した場合にechoコマンドが標準エラーに出力します。次に、結合された標準エラー出力が$resに保存され、後で調査されます。これが、すべてのプロセスの標準エラーが標準出力にリダイレクトされる理由でもあります。また、その出力を/dev/nullに送信するか、何か問題が発生したことを示す別のインジケーターとして残すこともできます。最後のコマンドの出力をどこかに保存する必要がある場合は、/dev/nullへの最後のリダイレクトをファイルに置き換えることができます。

このコンストラクトをさらに使用して、これが本当にすべきことを確信させるために、./a./b、および./cを、echocat、およびexitを実行するサブシェルに置き換えました。これを使用して、このコンストラクトが実際にすべての出力をあるプロセスから別のプロセスに転送し、エラーコードが正しく記録されることを確認できます。

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
8
josch