내 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
그러나 이로 인해 최소한 두 가지 이해 문제가 발생합니다.
tee
또는 의 오류 상태(반환 값)는 무엇입니까pee
? 매뉴얼 페이지에서 이에 대한 정확한 설명을 찾지 못했습니다. 소비 명령 중 하나가 실패하면 하드 코딩된 오류 상태를 반환합니까, 아니면 소비 명령의 오류 상태(예: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 설정 등 최대 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
또는 두 가지 접근 방식을 결합하여 표준 구문을 얻고 보너스로 종료 상태에 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 신호를 무시하고 파이프가 손상된 경우 파이프에 더 많은 데이터를 쓰려는 시도를 중지합니다).-p
tee
tee ...
다른 구현의 경우 비슷한 동작을 얻기 위해 대체할 수 있습니다 (trap '' PIPE; exec tee ...)
( tee
중단하지 않더라도 파이프 파손에 대한 오류 메시지가 표시될 수 있음).