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


97

現在、次のようなスクリプトがあります

./a | ./b | ./c

a、b、cのいずれかがエラーコードで終了した場合に、エラーメッセージを出力して停止し、不正な出力を転送しないように変更します。

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


7
&&|「前のコマンドが成功した場合にのみパイプを続行する」ことを意味するようなものが本当に必要です。|||「前のコマンドが失敗した場合にパイプを続行する」という意味もあるかもしれません(そしておそらくBash 4のようなエラーメッセージをパイプします|&)。
追って通知があるまで一時停止。

6
ので@DennisWilliamson、あなたは「パイプを止める」ことができないabcコマンドが順次なく、並列で実行されていません。換言すれば、データが順次から流れac、実際のabおよびcコマンドは、同時に(略)開始します。
Giacomo

回答:


20

最初のコマンドが成功するまで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シェルの古いバージョンでは、先行する「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のトラップのみを削除する必要があります(プロセスはとにかく終了するため、他のシグナルをそのままにしておくことができます)。

元のパイプラインでは、「c」が「a」が終了する前に「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'を使用した単純な実験では効果がありませんでした。


1
mktempまたはの使用をお勧めしtempfileます。
追って通知があるまで一時停止。

@Dennis:はい、mktempやtmpfileなどのコマンドに慣れる必要があります。私がそれを学んだとき、それらはシェルレベルでは存在しませんでした。簡単なチェックをしましょう。MacOS Xでmktempを見つけました。Solarisにmktempがありますが、これはGNUツールをインストールしたためです。mktempは旧式のHP-UXにあるようです。プラットフォーム間で機能するmktempの共通の呼び出しがあるかどうかはわかりません。POSIXはmktempもtmpfileも標準化していません。アクセス権のあるプラットフォームでtmpfileが見つかりませんでした。その結果、ポータブルシェルスクリプトでコマンドを使用できなくなります。
ジョナサンレフラー、

1
使用trapする場合、ユーザーはいつでもSIGKILLプロセスに送信してすぐに終了できることに注意してください。その場合、トラップは有効になりません。システムで停電が発生した場合も同様です。一時ファイルを作成するときは、必ず使用してmktempください。これにより、ファイルがどこかに置かれ、再起動後(通常はに/tmp)クリーンアップされます。
josch 2016年

155

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

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


私は本当に便利なpipefailについて知りませんでした。
フィルジャクソン

インタラクティブなシェルではなく、スクリプトに組み込むことを目的としています。-eを設定するように動作を簡略化できます。false; シェルプロセスも終了します。これは予想される動作です;)
Michel Samia 2013年

11
注:これでも3つのスクリプトがすべて実行され、最初のエラーでパイプが停止することはありません。
hyde

1
@ n2liquid-GuilhermeVieira Btw、「異なるバリエーション」とは、具体的にはset(合計4つの異なるバージョンの)の一方または両方を削除し、それが最後のの出力にどのように影響するかを確認することechoです。
ハイド2014年

1
@josch googleからbashでこれを行う方法を検索しているときにこのページを見つけましたが、「これはまさに私が探しているものです」です。回答に賛成する多くの人々が同様の思考プロセスを経て、タグとタグの定義をチェックしないのではないかと思います。
Troy Daniels

43

${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

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


11
これはbashishです---これはbashの拡張機能であり、Posix標準の一部ではないため、ダッシュやアッシュなどの他のシェルではサポートされていません。つまり#!/bin/sh、を起動するスクリプトで使用しようとすると、問題が発生する可能性があります。これは、shbashでないと機能しないためです。(#!/bin/bash代わりに使用することを覚えておけば、簡単に修正できます。)
David Given

2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'$PIPESTATUS[@]-0とスペースのみが含まれる場合は0を返します(パイプ内のすべてのコマンドが成功した場合)。
MattBianco

@MattBiancoこれは私にとって最高のソリューションです。また、&&でも機能しcommand1 && command2 | command3ます。たとえば、これらのいずれかが失敗した場合、ソリューションはゼロ以外を返します。
DavidC

8

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

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

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。これを使用して、この構成が実際にすべての出力を1つのプロセスから別のプロセスに転送すること、およびエラーコードが正しく記録されることを確認できます。

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.