$ PIPESTATUSをtee(またはpee)コマンドで使用できますか?

$ PIPESTATUSをtee(またはpee)コマンドで使用できますか?

私の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]}それぞれがエラー状態を反映するか、エラー状態であると仮定するのが論理的である。teepee

しかし、これは少なくとも2つの理解問題を引き起こす。

  1. teeまたはのエラー状態(戻り値)は何ですかpee?マニュアルページでこれの正確な説明が見つかりませんでした。消費コマンドの1つが失敗した場合は、ハードコードされたエラー状態を返しますか、それとも消費コマンドのエラー状態(たとえばssh)を渡しますか?前者の場合、どの消費者命令が原因であるかをどのように知ることができますか?後者の場合、どのエラー状態が渡されますか?まず失敗するコマンドでしょうか?

  2. 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

これは$PIPESTATUSbashism を避けるか>(...)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シグナルを無視し、パイプが破損した場合にパイプにデータを書き込む試みを中止します)。-ptee

tee ...他の実装では、同様の動作を得るために置き換えることができます(trap '' PIPE; exec tee ...)tee中断しなくてもパイプ破損に関するエラーメッセージが表示されることがあります)。

関連情報