$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

그러나 이로 인해 최소한 두 가지 이해 문제가 발생합니다.

  1. tee또는 의 오류 상태(반환 값)는 무엇입니까 pee? 매뉴얼 페이지에서 이에 대한 정확한 설명을 찾지 못했습니다. 소비 명령 중 하나가 실패하면 하드 코딩된 오류 상태를 반환합니까, 아니면 소비 명령의 오류 상태(예: 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 설정 등 최대 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

또는 두 가지 접근 방식을 결합하여 표준 구문을 얻고 보너스로 종료 상태에 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프로세스 중 하나가 모든 입력을 읽지 않고 종료되면 SIGPIPE로 인해 종료될 수 있으며 이는 다른 프로세스도 일부 입력을 잃을 수 있음을 의미합니다 . 따라서 종료 상태를 확인하는 것도 중요할 수 있습니다.

@UncleBilly가 이미 지적했듯이 GNU 구현을 사용하면 이 옵션을 사용하여 이 문제를 해결할 tee수 있습니다 (이는 SIGPIPE 신호를 무시하고 파이프가 손상된 경우 파이프에 더 많은 데이터를 쓰려는 시도를 중지합니다).-ptee

tee ...다른 구현의 경우 비슷한 동작을 얻기 위해 대체할 수 있습니다 (trap '' PIPE; exec tee ...)( tee중단하지 않더라도 파이프 파손에 대한 오류 메시지가 표시될 수 있음).

관련 정보