私のbashスクリプトはパイプラインを頻繁に使用し、パイプラインのどの段階でエラーが発生するかを知りたいと思います。これらの断片の基本構造は次のとおりです。
#!/bin/bash
ProduceCommand 2>/dev/null | ConsumeCommand >/dev/null 2>&1
PipeErrors=("${PIPESTATUS[@]}")
[[ "${PipeErrors[0]}" -eq '0' ]] || { HandleErrorInProduceCommand; }
[[ "${PipeErrors[1]}" -eq '0' ]] || { HandleErrorInConsumeCommand; }
tee
さて、(うーんと最初に)またはどちらかが利用可能であれば、それは良いような状況にありますpee
。しかし、$PIPESTATUS
このコマンドを使用するとどうなりますか?例えば:
#!/bin/bash
ProduceCommand 2>/dev/null | tee >(ConsumeCommand1) >(ConsumeCommand2) >/dev/null 2>&1
PipeErrors=("${PIPESTATUS[@]}")
または
#!/bin/bash
ProduceCommand 2>/dev/null | pee ConsumeCommand1 ConsumeCommand2 2>/dev/null
PipeErrors=("${PIPESTATUS[@]}")
私はどちらの場合も${PipeErrors[0]}
エラー状態を反映していると思いますProduceCommand
。さらに、${PipeErrors[1]}
それぞれがエラー状態を反映するか、エラー状態であると仮定するのが論理的である。tee
pee
しかし、これは少なくとも2つの理解問題を引き起こす。
tee
またはのエラー状態(戻り値)は何ですかpee
?マニュアルページでこれの正確な説明が見つかりませんでした。消費コマンドの1つが失敗した場合は、ハードコードされたエラー状態を返しますか、それとも消費コマンドのエラー状態(たとえばssh
)を渡しますか?前者の場合、どの消費者命令が原因であるかをどのように知ることができますか?後者の場合、どのエラー状態が渡されますか?まず失敗するコマンドでしょうか?AFAIK、bash、または
tee
コマンドpee
自体は、それぞれ内部でパイプ(fifos)を使用して出力をProduceCommand
消費コマンドにインポートします。これは、(最初とこの場合のみ)受信側がパイプ自体であるパイプがあることを意味します。これは上記のサンプルコードには影響しません$PipeErrors
が、実際にはわかりません。
誰かがこれを説明できますか?
答え1
エラー状態(戻り値)とは何ですか?
tee
すべてのデータをすべての出力ファイルにコピーできる場合は0、コピーできない場合は> 0です。より仕様。これGNU coreutilsの実装tee
書き込み中にエラーを無視する追加のオプションがあります。管路(実装に使用したものと同じ>(...)
):
$ seq 1024 | tee >(false) >/dev/null; echo $?
141
$ seq 1024 | tee -p >(false) >/dev/null; echo $?
0
わかるオプションはありませんどの出力が失敗しました(存在する場合)[1]。
>(..)
しかし、あなたの質問は、プロセスオーバーライドで実行されるコマンドの終了状態がどのように反映されるかPIPESTATUS
についてです。可能何らかの方法で実装されますPIPESTATUS
。
正解はいいえ。
まず、パイプラインコマンドではなくバックグラウンドコマンド>(...)
に似ていることに注意してください。次のスニペットから:... &
...|...
... | tee >(cmd ...) | ...; echo ${PIPESTATUS[@]}
cmd
実行すると完了する保証はありませんecho ${PIPESTATUS[@]}
。
しかし、いくつかの限られた場合を除いて、それらはそれらから得られず、...&
それらwait
からそれらの状態も取得できないので、まったく同じではありません。$!
いいえtee
他の外部コマンドとの使用を含める:
$ bash -c 'echo 1 | tee >(sleep 2; sed s/1/2/); wait; echo DONE'
1
DONE
$
<after two seconds>
2
ご覧のとおり、メインシェルtee
とメインシェルは、コマンドが実行される前に完了します>(...)
。
[1]同様のコマンドがpee
出力「サブコマンド」自体を実行しています(そして完了するのを待っています)。できるより賢明に、サブコマンドが失敗した終了状態を反映します(たとえば、最初のサブコマンドのビット1の設定、2番目のサブコマンドのビット2の設定など、最大8つのサブコマンドの設定)。しかし、そうしませんでした。
答え2
いつでも次のことができます。
{
{
ProduceCommand 2>/dev/null 3>&- ||
HandleErrorInProduceCommand >&3 3>&-
} |
tee >(
ConsumeCommand1 3>&- ||
HandleErrorInConsumer1 >&3 3>&-
) >(
ConsumeCommand2 3>&- ||
HandleErrorInConsumer2 >&3 3>&-
) > /dev/null
} 3>&1
プロデューサとコンシューマを起動する各サブシェルのエラーを処理します。
エラーハンドラの出力(存在する場合)がパイプを通過したくないので、エラーハンドラの元のstdoutを復元できるように、stdoutをfd 3にコピーします。
エラーハンドラをデフォルトのシェルプロセス内で実行するには(つまり、シャットダウンできるように)、これらのサブシェルにいくつかのコマンド代替パイプを介してシャットダウンステータスを親シェルにパイプさせることができます。
producer_status=-1
consumer1_status=-1
consumer2_status=-1
{
eval "$(
{
{
ProduceCommand 2>/dev/null 4>&-
echo "producer_status=$?" >&4
} | tee >(
ConsumeCommand1 4>&-
echo "consumer1_status=$?" >&4
) >(
ConsumeCommand2 4>&-
echo "consumer2_status=$?" >&4
)
} 4>&1 >&3 3>&-
)"
} 3>&1
[ "$producer_status" -eq 0 ] || HandleErrorInProduceCommand
[ "$consumer1_status" -eq 0 ] || HandleErrorInConsumer1
[ "$consumer2_status" -eq 0 ] || HandleErrorInConsumer2
これは$PIPESTATUS
bashism を避けるか>(...)
kshism を避け、通常のパイプに置き換えることができます。
{
ProduceCommand 2>/dev/null |
{
tee /dev/fd/4 |
ConsumeCommand1 4>&-
} 4>&1 >&3 3>&- |
ConsumeCommand2 3>&-
} 3>&1
producer_status=${PIPESTATUS[0]}
consumer1_status=${PIPESTATUS[1]}
consumer2_status=${PIPESTATUS[2]}
[ "$producer_status" -eq 0 ] || HandleErrorInProduceCommand
[ "$consumer1_status" -eq 0 ] || HandleErrorInConsumer1
[ "$consumer2_status" -eq 0 ] || HandleErrorInConsumer2
あるいは、2つのアプローチを組み合わせて標準構文を取得し、ボーナスで終了ステータスにsh
アクセスすることもできます。tee
producer_status=-1
tee_status=-1
consumer1_status=-1
consumer2_status=-1
{
eval "$(
{
{
ProduceCommand 2>/dev/null 4>&-
echo "producer_status=$?" >&4
} 3>&- |
{
{
tee /dev/fd/5 4>&-
echo "tee_status=$?" >&4
} |
ConsumeCommand1 4>&-
echo "consumer1_status=$?" >&4
} 5>&1 >&3 3>&- |
ConsumeCommand2 >&3 3>&- 4>&-
echo "consumer2_status=$?" >&4
} 4>&1
)"
} 3>&1
[ "$producer_status" -eq 0 ] || HandleErrorInProduceCommand
[ "$tee_status" -eq 0 ] || HandleErrorInTee
[ "$consumer1_status" -eq 0 ] || HandleErrorInConsumer1
[ "$consumer2_status" -eq 0 ] || HandleErrorInConsumer2
tee
プロセスの1つがすべての入力を読み取らずに終了すると、SIGPIPEによって終了する可能性があります。これは、他のプロセスが一部の入力を失う可能性があることを意味します。したがって、終了ステータスを確認することも重要です。
@UncleBillyがすでに指摘したように、GNU実装を使用すると、このオプションを使用してこの問題を解決tee
できます(これはSIGPIPEシグナルを無視し、パイプが破損した場合にパイプにデータを書き込む試みを中止します)。-p
tee
tee ...
他の実装では、同様の動作を得るために置き換えることができます(trap '' PIPE; exec tee ...)
(tee
中断しなくてもパイプ破損に関するエラーメッセージが表示されることがあります)。