関数にエラーをパイプすると、1 回の失敗後にコマンドが無視される

関数にエラーをパイプすると、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

原因はあなたのlog関数にあります。関数が行うことは、1 行を読み取り、その行が空でない場合はタイムスタンプを出力し、次に行の内容を出力します。関数が行うことはそれだけです。1 行処理したら、戻ります。

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

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

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

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

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

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

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

あるいは、プロセス置換パイプの代わりに、エラー出力を別のプロセスにパイプします。プロセス置換にはパイプとは少し異なる注意点があります。 のエラーはlog実質的に無視され、コマンドはlog完了する前に返される場合があります (bash ではlogバックグラウンドで実行されます)。

set -e
copy … 2> >(log)

¹ほぼそうです。IFS= read -r INこれ以上読む必要はなく、行を間違えることもないでしょう。

関連情報