tee + cat: 출력을 여러 번 사용한 다음 결과를 연결합니다.

tee + cat: 출력을 여러 번 사용한 다음 결과를 연결합니다.

예를 들어 특정 명령을 호출하면 echo해당 명령의 결과를 다른 여러 명령에 사용할 수 있습니다 tee. 예:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

cat을 사용하면 여러 명령의 결과를 수집할 수 있습니다. 예:

cat <(command1) <(command2) <(command3)

나는 두 가지를 동시에 수행하여 tee다른 것(예: 내가 작성한 것)의 출력에서 ​​이러한 명령을 호출 echo한 다음 모든 결과를 단일 출력으로 수집할 수 있기를 원합니다 cat.

command1결과를 순서대로 유지하는 것이 중요합니다. 즉, command2, 및 출력의 행은 command3인터리브되어서는 안 되고 명령에 따라 정렬되어야 합니다(발생하는 경우 cat).

cat아마도 및보다 더 나은 옵션이 있을 수 있지만 tee이것이 내가 지금까지 알고 있는 옵션입니다.

입력 및 출력 크기가 클 수 있으므로 임시 파일 사용을 피하고 싶습니다.

어떻게 해야 하나요?

PD: 또 다른 문제는 이것이 루프에서 발생하여 임시 파일을 처리하기가 더 어렵다는 것입니다. 이것은 내 현재 코드이며 작은 테스트 사례에서는 잘 작동하지만 내가 이해할 수 없는 방식으로 auxfile에서 읽고 쓸 때 무한 루프가 생성됩니다.

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

auxfile 읽기와 쓰기가 겹쳐서 모든 것이 폭발하는 것 같습니다.

답변1

peeGNU stdbuf와 다음의 조합을 사용할 수 있습니다.더 많은 유틸리티:

echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output

popen(3)이 3개의 쉘 명령줄을 입력하고 3 개를 모두 fread입력 fwrite하면 최대 1M까지 버퍼링됩니다.

아이디어는 최소한 입력만큼 큰 버퍼를 갖는 것입니다. 이렇게 하면 세 개의 명령이 동시에 시작되더라도 pee pclose세 개의 명령이 차례로 시작되는 경우에만 입력이 표시됩니다.

각 실행마다 pclose버퍼 pee를 명령으로 플러시하고 종료될 때까지 기다립니다. cmdx이는 이러한 명령이 입력을 받기 전에 아무 것도 출력을 시작하지 않는 한(그리고 상위 반환 후에 출력을 계속할 수 있는 프로세스를 포크하지 않는 한) 이 세 명령의 출력이 인터리브되지 않음을 보장합니다 .

실제로 이는 메모리의 임시 파일을 사용하는 것과 약간 비슷하지만 이 3개의 명령이 동시에 실행된다는 단점이 있습니다.

pee동시에 명령을 시작하지 않으려면 쉘 함수로 작성할 수 있습니다 .

pee() (
  input=$(cat; echo .)
  for i do
    printf %s "${input%.}" | eval "$i"
  done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out

그러나 zshNUL 문자가 아닌 이진 입력에서는 쉘이 실패합니다.

이는 임시 파일 사용을 방지하지만 전체 입력이 메모리에 저장된다는 의미입니다.

그럼에도 불구하고 입력을 메모리나 임시 파일 어딘가에 저장해야 합니다.

이것은 작업을 수행하기 위해 여러 개의 간단한 도구가 협력하도록 하는 Unix 아이디어의 한계를 보여주기 때문에 실제로 매우 흥미로운 질문입니다.

여기서는 작업을 완료하기 위한 몇 가지 도구가 있기를 바랍니다.

  • 소스 명령(여기 echo)
  • 스케줄러 명령( tee)
  • 일부 필터링 명령( cmd1, cmd2, cmd3)
  • 및 집계 명령( cat).

동시에 실행하여 처리하고 싶은 데이터가 나오자마자 처리할 수 있다면 좋을 것 같습니다.

필터 명령의 경우 간단합니다.

src | tee | cmd1 | cat

모든 명령은 동시에 실행되며 cmd1데이터가 사용 가능해지면 즉시 데이터를 씹기 시작합니다.src

이제 세 가지 필터 명령을 사용하여 동일한 작업을 수행할 수 있습니다. 즉, 모두 동시에 시작하고 파이프하는 것입니다.

               ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
               ┃   ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃
               ┃   ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

우리는 이것을 비교적 쉽게 할 수 있다명명된 파이프:

pee() (
  mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
  { tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
  eval "$1 < tee-cmd1 1<> cmd1-cat &"
  eval "$2 < tee-cmd2 1<> cmd2-cat &"
  eval "$3 < tee-cmd3 1<> cmd3-cat &"
  exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

} 3<&0(위의 내용은 리디렉션에서 반대쪽 끝( )도 열릴 때까지 파이프 차단이 열리는 것을 방지하기 위해 사용한다는 사실을 설명하기 위한 것입니다 .)&stdin/dev/null<>cat

또는 명명된 파이프를 피하려면 coproc을 사용하는 것이 더 어려울 것입니다 zsh.

pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    eval "coproc $cmd $ci $co"

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

이제 문제는 모든 것이 준비되고 연결되면 데이터가 계속 흐를 것인가입니다.

두 가지 제한 사항이 있습니다.

  • tee모든 출력은 동일한 속도로 제공되므로 가장 느린 출력 파이프의 속도로만 데이터를 보낼 수 있습니다.
  • cat두 번째 파이프(위 이미지의 파이프라인 6)에서 읽기는 첫 번째 파이프(5)에서 모든 데이터를 읽은 후에만 시작됩니다.

이는 cmd1파이프 6이 완료될 때까지 데이터가 파이프 6으로 흐르지 않음을 의미합니다. 그리고 위의 시나리오와 유사하게 tr b B이는 파이프 3에도 데이터가 흐르지 않는다는 것을 의미할 수 있습니다. 이는 데이터가 tee가장 많은 수의 파이프로 흐르기 때문에 파이프 2, 3 또는 4 중 어느 것에도 데이터가 흐르지 않는다는 것을 의미합니다. 3개 파이프 모두에 파이프가 포함되어 있습니다. 전송 속도가 느립니다.

실제로 이러한 파이프의 크기는 null이 아니므로 일부 데이터가 통과할 수 있습니다. 적어도 내 시스템에서는 다음과 같이 작동하도록 할 수 있습니다.

yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c

게다가,

yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

우리는 교착상태에 이르렀고 상황은 다음과 같습니다.

               ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
               ┃   ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃   ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃   ┃
               ┃   ┃██████████┃cmd3┃██████████┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

파이프 3과 파이프 6(각각 64kiB)을 채웠습니다. tee해당 추가 바이트를 읽었고 이를 공급 cmd1했지만

  • cmd2이제 파이프 3에 대한 쓰기가 삭제를 기다리고 있기 때문에 차단되었습니다.
  • cmd2파이프 6에 대한 쓰기가 차단되어 플러시할 수 없습니다. cat플러시를 기다리고 있습니다.
  • cat파이프 5에 더 이상 입력이 없을 때까지 기다리고 있으므로 비울 수 없습니다.
  • cmd1cat에서 더 많은 입력을 기다리고 있기 때문에 더 이상 입력이 없다고 말할 방법이 없습니다 tee.
  • 그리고 차단되었기 때문에 더 이상 입력이 없다고 tee말할 수 있는 방법이 없습니다 .cmd1

종속성 주기가 있으므로 교착 상태가 발생합니다.

이제 해결책은 무엇입니까? 더 큰 파이프 3과 4(모든 출력을 포함할 만큼 충분히 큼 src)가 이를 수행할 수 있습니다. 예를 들어, 와 사이에 데이터를 삽입하여 pv -qB 1G대기하고 읽어 들이면 최대 1G까지 데이터를 저장할 수 있습니다 . 그러나 이는 두 가지를 의미합니다.teecmd2/3pvcmd2cmd3

  1. 이는 많은 메모리를 차지하며 반복될 수 있습니다.
  2. cmd2데이터 처리는 실제로 cmd1이 완료된 후에만 시작되므로 3개의 명령이 모두 함께 작동하는 것을 허용하지 않습니다 .

두 번째 문제에 대한 해결책은 파이프 6과 7도 더 크게 만드는 것입니다. 소비하는 만큼의 출력을 가정 cmd2하고 생성하면 더 이상 메모리가 소비되지 않습니다.cmd3

첫 번째 질문에서 중복 데이터를 피하는 유일한 방법은 스케줄러 자체에 데이터 보존을 구현하는 것입니다. 즉, tee가장 빠른 출력 속도로 데이터를 제공할 수 있는 변형을 구현하는 것입니다(스케줄러에 공급할 데이터를 저장). 속도가 느려집니다.) 실제로 사소한 것은 아닙니다.

따라서 결국 프로그래밍 없이 합리적으로 달성할 수 있는 최선의 방법은 아마도 다음과 같습니다(Zsh 구문).

max_hold=1G
pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    if ((n)); then
      eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
    else
      eval "coproc $cmd $ci $co"
    fi

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

답변2

귀하가 제안한 작업은 기존 명령을 사용하여 쉽게 수행할 수 없으며 어쨌든 의미가 없습니다. (Unix/Linux에서) 파이프의 전체 아이디어는 |메모리 버퍼가 가득 찰 때까지 (최대) 출력을 쓴 다음, cmd1 | cmd2버퍼가 빌 때까지 (최대) 버퍼에서 데이터를 읽는 것입니다. 즉, 동시에 실행하면 둘 사이에 "전송 중인" 데이터의 양이 제한되어 있을 필요가 없습니다. 여러 입력을 하나의 출력에 연결하려는 경우 판독기 중 하나가 다른 판독기보다 뒤처지면 다른 판독기를 중지하거나(병렬로 실행하는 이유가 무엇입니까?) 아직 읽지 않은 지연자를 배치합니다. 숨겨져 있습니다(그러면 중간 파일이 없다는 게 무슨 의미가 있나요?). 또한 전체 동기화는cmd1cmd2cmd1cmd2많은더 복잡한.

거의 30년 동안의 Unix 경험을 통해 이와 같은 다중 출력 파이프로 인해 실제로 이점을 얻을 수 있는 상황은 기억나지 않습니다.

cmd1오늘날에는 인터리브 방식이 아닌 여러 출력을 단일 스트림으로 결합할 수 있습니다(출력은 어떻게 cmd2인터리브해야 합니까? 한 번에 한 줄씩 작성합니까? 10바이트를 차례로 작성합니까? 대체 "문단"이 어떤 방식으로 정의됩니까? 누가 그러지 않겠어요) 오랫동안 아무것도 쓰지 않고 지내나요? 이 모든 것은 처리하기가 매우 어렵습니다.) 예를 들어, (cmd1; cmd2; cmd3) | cmd4프로그램 cmd1, cmd2및 를 순서대로 cmd3실행하고 출력을 입력으로 전송하여 수행됩니다 cmd4.

답변3

겹치는 문제의 경우 Linux에서( bash유무에 관계 zsh없이 ksh93) 다음을 수행할 수 있습니다.

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

(...)대신 을 사용하여 {...}각 반복에서 새 프로세스를 가져오면 새 를 가리키는 새 fd 3을 가질 수 있습니다 auxfile. < /dev/fd/3이는 현재 삭제된 파일에 액세스하는 방법입니다. Linux 이외의 시스템에서는 작동하지 않습니다. < /dev/fd/3예를 들어 dup2(3, 0), fd 0은 파일 끝에 커서가 있는 쓰기 전용 모드로 열립니다.

일부 함수의 중첩 포크를 방지하려면 다음과 같이 작성할 수 있습니다.

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

껍질이 알아서 처리해줄게지원반복당 fd 3. 그러나 결국에는 파일 설명자가 빨리 부족해집니다.

이렇게 하는 것이 더 효율적일 수도 있지만 다음과 같이 하세요.

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

즉, 리디렉션을 중첩하지 마세요.

답변4

이러한 제한 사항으로 인해 일반적인 문제에 대한 해결책이 보이지 않습니다.

결과를 순서대로 유지하는 것이 중요합니다. 즉, command1, command2 및 command3의 출력 행은 인터리브되어서는 안 되며 명령별로 정렬되어야 합니다(cat의 경우처럼).

:

입력 및 출력 크기가 클 수 있으므로 임시 파일 사용을 피하고 싶습니다.

각각의 입력과 출력이 command*RAM+디스크보다 크다고 가정해 보겠습니다. 어떻게든 command2+3출력할 때 출력을 저장해야 command1하거나, 입력을 저장해야 합니다. 입력 및 출력이 RAM+디스크보다 크면 이것이 어디에 저장될 수 있는지 알 수 없습니다.

이는 제한 사항이 완화되고 총 출력이 디스크보다 적은 동안 디스크에 저장이 허용되는 경우 수행할 수 있습니다.

입력이 크고 교착 상태 또는 메모리 부족 위험을 원하지 않는 경우:

echo "Hello world!" | tee >(command1 >out1) >(command2 >out2) >(command3 >out3) >/dev/null
cat out1 out2 out3

이것이 기본 아이디어입니다 parallel --tee.

echo "Hello world!" | parallel --pipe --tee ::: command1 command2 command3
cat bigger-than-ram-file | parallel --pipe --tee ::: command1 command2 command3

parallel또한 stderr를 처리하고 임시 파일을 정리합니다.

인터리빙에 대한 제한을 완화하면 임시 파일을 위한 공간이 필요하지 않습니다. 즉, RAM+디스크보다 더 큰 출력을 가질 수 있습니다. 이는 전체 라인을 버퍼링하지만 전체 출력을 버퍼링하지는 않습니다.

cat bigger-than-ram+disk-file |
  parallel --line-buffer --pipe --tee ::: command1 command2 command3

관련 정보