web-dev-qa-db-ja.com

関数への配管エラーにより、1回の失敗後にコマンドが無視されます

コンテキスト:ファイルをコピーするBashスクリプトがあります

function log () {
    read IN
    if [ "$IN" == "" ]; then
        :
    else

        echo "$datetime"$'\t'"$IN" | tee -a logfile
    fi
}

function copy () {
    command cp -L --parents $@
}

...
copy -R /etc . 2>&1 | log
...

問題:cp -L --parents -R /etc 2>&1を手動で実行すると、約10回の失敗(シンボリックリンクの破損が予想されます)が発生し、/ etc全体がコピーされました。

ただし、スクリプトを実行すると、1つの障害のみが報告され、/ etcは1つの障害が発生したポイントにのみコピーされます。

トラブルシューティングを試みる際に、スクリプトから2>&1を削除するだけで、期待どおりにコピーが続行されました。

質問:私のlog関数が問題を引き起こしていますか、それともこれはスクリプトの記述方法における構文上の問題ですか(スクリプトを壊すことはありませんが)?

1
Timothy Wong

log関数が原因です。 1行¹を読み取り、その行が空でない場合は、タイムスタンプを出力してから、行の内容を出力します。それがすべてです:それが1行を処理されると、それは戻ります。

cpが最初のエラーメッセージを出力すると、log関数がそれを読み取って処理します。その後、log関数が戻るため、パイプの右側のプロセスが終了し、パイプの読み取り側が閉じます。 cpが2番目のエラーメッセージを発行すると、閉じたパイプに書き込もうとします。これにより、パイプは SIGPIPEシグナル で停止します。標準エラーは行バッファリングされているため(デフォルトでは、cpはそれを変更しようとしません)、バッファリングは機能しません。

入力のすべての行を処理するには、ループ内にreadが必要です。

log () {
    while IFS= read -r IN; do
        echo "$datetime"$'\t'"$IN"
    done | tee -a logfile >&2
}

また、read呼び出しを IFS= read -r に修正して、実際に1行を読み取りました。無意味な空の行の特別な処理を削除しました(入力に空の行はありません)。入力が空(入力の行がゼロ)の場合を処理するためにそれを入れたと思いますが、それを処理する正しい方法は、readコマンドの戻りステータスを確認することです。また、logはエラーメッセージの処理に使用されるため、標準エラーに出力するように修正しました。

これを行う他の方法については、 コマンドからの出力の各行にタイムスタンプを付加する を参照してください。

コマンドをパイプの左側に配置すると、大きな欠点があることに注意してください:終了ステータスが無視されます。したがって、cpが失敗した場合、スクリプトは問題なく続行されます。エラーはどこかにログに記録されますが、後続のコマンドは正常に実行され、ログを確認する必要があるという事実を警告するものは何もありません。 bash、ksh、またはzshでは、set -eに加えて pipefail option を設定して、コマンドが失敗するとすぐにスクリプトがエラーステータスで終了するようにすることができます。パイプラインの左側にあります。

set -o errexit -o pipefail
copy … |& log

または、パイプの代わりに プロセス置換 を使用して、エラー出力を別のプロセスにパイプします。プロセス置換には、パイプとは少し異なる警告があります。 logのエラーは事実上無視され、コマンドはlogが完了する前に戻る可能性があります(bashでは、logはバックグラウンドで実行されます)。

set -e
copy … 2> >(log)

¹ ほとんどの場合、これ以上読み上げたり、行を壊したりしないようにするには、IFS= read -r INが必要です。