Bash 코드에서
command1 | tee >(command2) | command3
command2
in의 출력과 var2
in의 출력을 캡처하고 싶습니다 .command3
var3
command1
다른 명령은 I/O 제약으로 인해 비용이 많이 들지만 command1
완료되기 전에 작업을 시작할 수 있습니다.
command2
합계의 출력 순서 command3
는 고정되어 있지 않습니다. 그래서 파일 설명자를 사용해 보았습니다.
read -r var2 <<< var3=(command1 | tee >(command2 >&3) | command3) 3>&1
또는
{read -u 3 -r var2; read -r var3} <<< command1 | tee >(command2 >&3) | command3
그러나 성공하지 못했습니다.
이 세 가지 명령을 병렬로 실행하여 임시 파일을 만들지 않고 결과를 다른 변수에 저장하는 방법이 있습니까?
답변1
그러면 cmd1의 출력을 cmd2 및 cmd3으로 파이프하고 cmd2 및 cmd3의 출력을 다른 변수로 가져오시겠습니까?
따라서 쉘에서 두 개의 파이프가 필요한 것 같습니다. 하나는 cmd2의 출력에, 다른 하나는 cmd3의 출력에 연결되며 쉘은 두 파이프에서 select()
/를 사용합니다.poll()
bash
그렇게 하지 않을 것입니다. 와 같은 더 높은 수준의 셸이 필요합니다 zsh
. zsh
원시 인터페이스는 없지만 pipe()
Linux에서는 일반 파이프에 대해 명명된 파이프 역할을 한다는 사실을 활용 /dev/fd/x
하고 아래와 유사한 것을 사용할 수 있습니다.쉘 리디렉션을 사용하여 동일한 파일 설명자 읽기/쓰기
#! /bin/zsh -
cmd1() seq 20
cmd2() sed 's/1/<&>/g'
cmd3() tr 0-9 A-J
zmodload zsh/zselect
zmodload zsh/system
typeset -A done out
{
cmd1 > >(cmd2 >&3 3>&-) > >(cmd3 >&5 5>&-) 3>&- 5>&- &
exec 4< /dev/fd/3 6< /dev/fd/5 3>&- 5>&-
while ((! (done[4] && done[6]))) && zselect -A ready 4 6; do
for fd (${(k)ready[(R)*r*]}) {
sysread -i $fd && out[$fd]+=$REPLY || done[$fd]=1
}
done
} 3> >(:) 5> >(:)
printf '%s output: <%s>\n' cmd2 "$out[4]" cmd3 "$out[6]"
답변2
귀하의 모든 요구 사항을 잘 이해하고 있다면 bash
각 명령에 대해 명명되지 않은 파이프를 만든 다음 각 명령의 출력을 해당 명명되지 않은 파이프로 리디렉션하고 마지막으로 Retrieve의 각 출력을 별도의 변수로 파이핑하여 이를 달성할 수 있습니다.
따라서 해결책은 다음과 같습니다.
: {pipe2}<> <(:)
: {pipe3}<> <(:)
command1 | tee >({ command2 ; echo EOF ; } >&${pipe2}) >({ command3 ; echo EOF ; } >&${pipe3}) > /dev/null &
var2=$(while read -ru ${pipe2} line ; do [ "${line}" = EOF ] && break ; echo "${line}" ; done)
var3=$(while read -ru ${pipe3} line ; do [ "${line}" = EOF ] && break ; echo "${line}" ; done)
exec {pipe2}<&- {pipe3}<&-
여기서 특별한 주의가 필요한 것은 다음과 같습니다.
- 이 구조의 사용법
<(:)
은 "이름이 지정되지 않은" 파이프를 여는 문서화되지 않은 Bash 트릭입니다. - 더 이상 출력이 없음을 루프에
echo EOF
알리려면 간단한 방법을 사용하십시오 .while
이는 이름 없는 파이프(일반적으로 모든 루프가 종료됨while read
)를 닫는 것만으로는 아무 소용이 없기 때문에 필요합니다. 이러한 파이프는 쓰기 및 읽기용으로 양방향이기 때문입니다. 읽기용과 쓰기용으로 하나씩 일반적인 몇 가지 파일 설명자로 열거나 변환하는 방법을 모르겠습니다.
이 예에서는 순수한 bash 접근 방식을 사용했습니다 ( 각 변수 3을 것을 제외하면tee
사용하는sed
while
var2="$(sed -ne '/^EOF$/q;p' <&${pipe2})"
소량의 데이터를 위한 린 솔루션
: {pipe2}<> <(:)
: {pipe3}<> <(:)
command1 | tee >({ command2 ; echo EOF ; } >&${pipe2}) >({ command3 ; echo EOF ; } >&${pipe3}) > /dev/null &
var2="$(sed -ne '/^EOF$/q;p' <&${pipe2})"
var3="$(sed -ne '/^EOF$/q;p' <&${pipe3})"
exec {pipe2}<&- {pipe3}<&-
대상 변수를 표시하려면 다음과 같이 IFS를 지워 토큰화를 비활성화해야 합니다.
IFS=
echo "${var2}"
echo "${var3}"
그렇지 않으면 출력에서 개행 문자가 손실됩니다.
위의 내용은 매우 깨끗한 솔루션처럼 보입니다. 불행히도 너무 높지 않은 출력에만 사용할 수 있으며 마일리지는 다를 수 있습니다. 테스트에서 약 530k의 출력에 문제가 있었습니다. 4k 제한 내에 있다면 (매우 보수적으로) 괜찮을 것입니다.
이 제한의 이유는 이러한 작업이 두 개라는 사실에 있습니다.명령 대체구문은 동기 작업입니다. 즉, tee
두 명령을 동시에 제공하고 명령이 수신 버퍼를 채울 때 모든 명령을 차단하는 것과는 반대로 첫 번째 할당이 완료된 후에만 두 번째 할당이 실행됩니다. 이중 자물쇠.
이 문제를 해결하려면 두 버퍼를 동시에 지우려면 약간 더 복잡한 스크립트가 필요합니다. 이렇게 하려면 while
두 파이프를 모두 반복합니다.회의편리하게 오세요.
모든 데이터 볼륨에 대한 보다 표준적인 솔루션
더 표준적인 Bashism은 다음과 같습니다.
declare -a var2 var3
while read -r line ; do
case "${line}" in
cmd2:*) var2+=("${line#cmd2:}") ;;
cmd3:*) var3+=("${line#cmd3:}") ;;
esac
done < <(
command1 | tee >(command2 | stdbuf -oL sed -re 's/^/cmd2:/') >(command3 | stdbuf -oL sed -re 's/^/cmd3:/') > /dev/null
)
여기서 두 명령의 행을 단일 표준 "stdout" 파일 설명자로 다중화한 다음 출력을 각 해당 변수에 병합하여 역다중화합니다.
주의:
- 인덱스 배열을 대상 변수로 사용: 이는 일반 변수에 추가하는 것만으로도 출력량이 많아지면 속도가 매우 느려지기 때문입니다.
- 명령을 사용하여
sed
각 출력 줄 앞에 문자열 "cmd2:" 또는 "cmd3:"(각각)을 추가하면 스크립트가 각 줄이 어떤 변수에 속하는지 알 수 있습니다. - 명령 출력을 위한 라인 버퍼링의 필수 사용
stdbuf -oL
: 이는 여기에 있는 두 명령이 동일한 출력 파일 설명자를 공유하기 때문입니다. 따라서 스트리밍 중인 경우 가장 일반적인 경쟁 조건에서 동시에 서로의 출력을 쉽게 덮어쓸 수 있습니다. 출력 데이터; 라인 버퍼 출력은 이를 방지하는 데 도움이 됩니다. - 또한 각 체인의 마지막 명령만 stdbuf, 즉 공유 파일 설명자에 직접 출력하는 명령(이 경우 각 commandX의 출력 앞에 고유 접두사를 추가하는 sed 명령)을 사용해야 합니다.
이러한 인덱스 배열을 올바르게 표시하는 안전한 방법은 다음과 같습니다.
for ((i = 0; i < ${#var2[*]}; i++)) ; do
echo "${var2[$i]}"
done
물론 직접 사용할 수도 있습니다 "${var2[*]}"
.
echo "${var2[*]}"
하지만 라인이 많으면 효율성이 그다지 높지 않습니다.
답변3
잘 작동하는 것 같은 것을 찾았습니다.
exec 3<> <(:)
var3=$(command1 | tee >(command2 >&3) | command3)
var2=$(while IFS= read -t .01 -r -u 3 line; do printf '%s\n' "$line"; done)
<(:)
파일 설명자 3에 대한 익명 파이프를 설정하고 command2
해당 출력을 파이핑하는 방식으로 작동합니다. 파일 설명자 3의 출력을 캡처 var3
하고 0.01초 동안 새 데이터가 수신되지 않을 때까지 마지막 줄을 읽습니다.command3
command2
익명 파이프에 의해 버퍼링되는 것으로 보이는 최대 65536바이트의 출력에만 작동합니다 .
솔루션의 마지막 줄이 마음에 들지 않습니다. 0.01초 동안 기다렸다가 버퍼가 비자마자 중지하는 것보다 한 번에 모든 내용을 읽는 편이 낫습니다. 하지만 더 좋은 방법은 모르겠습니다.
답변4
4.0부터는 가능합니다. bash는 쉘 예약어 coproc을 추가합니다. 백그라운드로 명령을 따르기 위해 하위 프로세스로 분기되지만(일반적으로 변수 전달이 허용되지 않음) 배열을 생성합니다(기본값은 COPROC이지만 이름을 지정할 수 있음). ${COPROC[0]}은 하위 프로세스 ${COPROC의 표준 입력에 연결됩니다.1이러한 프로세스는 작업으로 작동할 수 있고 비동기식이므로 tee를 사용하여 두 코루틴으로 파이프하고 별도의 파일로 출력한 다음 "${ 코루틴을 먼저 호출할 수 있습니다.1} ${코루틴 초1}" 이는 파이프 안에 있을 필요도 없기 때문에 매우 좋습니다.
bash coproc command_1 { command1 arg1 arg2 arg3 >> command_1_output.txt } coproc command_2 { command2 arg1 arg2 arg3 >> command_2_output.txt } othercommand tee>^${command_11}" >&"${command_21}" 읽기 -r 결과1 <&"${command_1[0]" 읽기 -r resluts2 <&"${command_2[0]" echo "$result1 $result2" | command3 >>결과 병합.txt
언급했듯이 이 솔루션은 아직 완성되지 않았으므로 충격을 받았지만 원칙은 타당하고 위의 답변과 유사합니다. 그러나 주제에 대한 좋은 기사를 안내하고 작업이 가능할 때 이 답변으로 돌아오겠습니다.