티 + 고양이 : 출력을 여러 번 사용한 다음 결과를 연결


18

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

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

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

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

나는 동시에 두 가지 작업을 모두 수행하고 싶기 때문에 tee다른 명령 (예 echo: 내가 쓴 것) 의 출력에서 ​​해당 명령을 호출 하고 모든 결과를 단일 출력에서 ​​수집하는 데 사용할 수 있습니다. cat.

순서대로 결과를 유지하는 것이 중요합니다,이 수단의 출력 라인 command1, command2및이 command3(가 함께 발생으로 얽혀 있지만, 명령과 마찬가지로 주문해서는 안된다 cat).

이보다 더 나은 옵션이있을 수 있습니다 cattee하지만 사람들은 내가 지금까지 알고있는 것들이다.

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

어떻게해야합니까?

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에서 읽고 쓰는 것이 겹쳐 져서 모든 것이 폭발하는 것 같습니다.


2
우리는 얼마나 큰 대화를하고 있습니까? 요구 사항에 따라 모든 것이 메모리에 유지됩니다. 결과를 순서대로 유지한다는 것은 command2 및 command3이 처리를 시작할 수 있기 전에 (먼저 메모리에 출력을 수집하지 않는 한) command1이 먼저 완료되어야하고 (아마도 전체 입력을 읽고 전체 출력을 인쇄했음을 의미 함) 의미합니다.
frostschutz

맞습니다. command2 및 command3의 입력 및 출력이 너무 커서 메모리에 보관할 수 없습니다. 스왑을 사용하는 것이 임시 파일을 사용하는 것보다 더 효과적이라고 기대했습니다. 내가 가진 또 다른 문제는 이것이 루프에서 발생하여 파일 처리를 더욱 어렵게한다는 것입니다. 단일 파일을 사용하고 있지만 현재 어떤 이유로 인해 파일을 읽고 쓰는 데 약간의 겹침이있어 파일이 무한대로 커질 수 있습니다. 너무 많은 세부 사항을 지루하지 않고 질문을 업데이트하려고합니다.
Trylks

4
임시 파일을 사용해야합니다. 입력 echo HelloWorld > file; (command1<file;command2<file;command3<file)또는 출력을 위해 echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. 티는 모든 명령이 동시에 작동하고 처리되는 경우에만 입력을 포크 할 수 있습니다. 하나의 명령이 잠 들면 (인터리빙을 원하지 않기 때문에) 입력으로 메모리를 채우지 않도록 모든 명령을 차단합니다.
frostschutz

답변:


27

당신은 GNU stdbuf의 및 이들의 조합으로 사용할 수 pee에서 moreutils를 :

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

popen(3)이 3 개의 셸 명령 행을 pee로 fread입력 한 다음 입력 fwrite을 모두 3 개 까지 입력 하면 최대 1M까지 버퍼링됩니다.

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

각각 pclosepee대해 버퍼를 명령으로 플러시하고 종료를 기다립니다. 따라서 해당 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아니라 열었다))를

또는 명명 된 파이프를 피하기 위해 zshcoproc을 사용 하면 조금 더 고통 스럽습니다.

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 첫 번째 데이터 (5)에서 모든 데이터를 읽었을 때 두 번째 파이프 (위의 그림에서 파이프 6)부터 읽기 시작합니다.

이는 데이터 cmd1가 완료 될 때까지 파이프 6에서 데이터가 흐르지 않음을 의미 합니다. 그리고 tr b B위 의 경우와 같이 데이터가 파이프 3으로 흐르지 않음을 의미 할 수 있습니다. 즉, 데이터가 tee모두 3의 가장 느린 속도로 공급 되므로 파이프 2, 3 또는 4 중 어느 것도 흐르지 않습니다 .

실제로 이러한 파이프는 널이 아닌 크기를 가지므로 일부 데이터는 통과 할 수 있으며 시스템에서 최소한 다음과 같이 작동하도록 할 수 있습니다.

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했지만

  • 파이프 3 cmd2을 비우기를 기다리는 동안 쓰기가 차단 되었습니다.
  • cmd2파이프 6의 쓰기가 차단 cat되어 비우기를 기다리는 중이므로 비울 수 없습니다.
  • cat 파이프 5에 더 이상 입력이 없을 때까지 대기 중이므로 비울 수 없습니다.
  • cmd1cat에서 더 많은 입력을 기다리고 있기 때문에 더 이상 입력이 없다고 말할 수 없습니다 tee.
  • 하고 tee말할 수 없다 cmd1거기가 막혀 있기 때문에 더 이상 입력은 ... 등등.

의존성 루프와 교착 상태가 있습니다.

자, 해결책은 무엇입니까? 더 큰 파이프 3과 4 (모든 src출력 을 포함하기에 충분히 큰 )가 그렇게합니다. 우리는 삽입하여 예를 들어 그렇게 할 수있는 pv -qB 1G사이 teecmd2/3위치를 pv기다리고 데이터의 1G까지 저장할 수 cmd2cmd3읽을 수 있습니다. 그것은 두 가지 의미가 있습니다.

  1. 잠재적으로 많은 양의 메모리를 사용하고 있습니다.
  2. cmd2실제로는 cmd1이 완료되었을 때만 데이터 처리를 시작하기 때문에 3 개의 명령이 모두 협력하지 못했습니다 .

두 번째 문제에 대한 해결책은 파이프 6과 7을 더 크게 만드는 것입니다. 그 가정 cmd2cmd3더 많은 메모리를 소비하지 않을 그들이 소비하는만큼 출력으로 생산합니다.

첫 번째 문제에서 데이터 복제를 피하는 유일한 방법은 디스패처 자체에서 데이터 보존을 구현하는 것입니다. 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

교착 상태는 임시 파일 사용을 피하기 위해 지금까지 찾은 가장 큰 문제입니다. 이 파일은 상당히 빠르지 만 어딘가에 캐시되는지 여부를 알지 못하고 디스크 액세스 시간이 두렵지 만 지금까지는 합리적입니다.
Trylks

6
여분 +1 :-) 좋은 ASCII 예술에 대한
커트 Pfeifle

3

기존 명령으로 쉽게 제안 할 수 없으며 어쨌든 의미가 없습니다. (파이프의 모든 생각 |유닉스 / 리눅스가)에 있다는 것입니다 메모리 버퍼를 채울 때까지 (최대) 글을 출력 한 다음이 비어 때까지 (대부분에서) 버퍼로부터 데이터를 읽어 실행됩니다. 즉, 동시에 실행되기 때문에 그들 사이에 "비행중인"제한된 양의 데이터를 가질 필요는 없습니다. 여러 입력을 단일 출력에 연결하려면 판독기 중 하나가 다른 출력기보다 지연되는 경우 다른 출력기를 중지하거나 (병렬로 실행되는 지점은 무엇입니까?) 지연 시간이 아직 읽지 않은 출력을 숨기십시오. (중간 파일이없는 요점은 무엇입니까?).cmd1 | cmd2cmd1cmd2cmd1cmd2 더 복잡한.

거의 30 년 동안 유닉스 경험을 쌓았을 때 그러한 다중 출력 파이프에 실제로 도움이 된 상황은 기억 나지 않습니다.

인터리브 방식이 아닌 오늘날 여러 개의 출력을 하나의 스트림으로 결합 할 수 있습니다 (어떻게 출력 cmd1하고 cmd2인터리브해야합니까? 한 줄을 차례로 바꾸는가? 10 바이트를 쓰는 데 걸리는가? 어떻게 든 정의 된 대체 "문단"? 오랫동안 아무것도 쓰지 않습니까?이 모든 것이 다루기가 복잡합니다). 그것은 예에 의해 수행되고 (cmd1; cmd2; cmd3) | cmd4, 프로그램 cmd1, cmd2cmd3다른 후 하나를 실행, 출력은 입력으로 전송됩니다 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가 new를 가리 키도록 할 수 있습니다 auxfile. < /dev/fd/3삭제 된 파일에 액세스하는 트릭입니다. 그것은 리눅스가 아닌 다른 시스템에서 작동하지 않습니다 < /dev/fd/3처럼 dup2(3, 0)그래서 파일의 끝 부분에 커서를 쓰기 전용 모드로 개방 될 것이다 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

즉, 리디렉션을 중첩하지 마십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.