tee + cat: sử dụng một đầu ra nhiều lần và sau đó ghép các kết quả


18

Nếu tôi gọi một số lệnh, ví dụ, echotôi có thể sử dụng kết quả từ lệnh đó trong một số lệnh khác với tee. Thí dụ:

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

Với con mèo tôi có thể thu thập kết quả của một số lệnh. Thí dụ:

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

Tôi muốn có thể làm cả hai việc cùng một lúc, để tôi có thể sử dụng teeđể gọi các lệnh đó trên đầu ra của một thứ khác (ví dụ như echotôi đã viết) và sau đó thu thập tất cả kết quả của chúng trên một đầu ra với cat.

Điều quan trọng là giữ các kết quả theo thứ tự, điều này có nghĩa là các dòng trong đầu ra của command1, command2command3không nên được đan xen, nhưng được sắp xếp như các lệnh (như nó xảy ra với cat).

Có thể có những lựa chọn tốt hơn catteenhưng đó là những lựa chọn mà tôi biết cho đến nay.

Tôi muốn tránh sử dụng các tệp tạm thời vì kích thước của đầu vào và đầu ra có thể lớn.

Làm thế nào tôi có thể làm điều này?

PD: một vấn đề khác là điều này xảy ra trong một vòng lặp, khiến việc xử lý các tệp tạm thời khó khăn hơn. Đây là mã hiện tại tôi có và nó hoạt động cho các testcase nhỏ, nhưng nó tạo ra các vòng lặp vô hạn khi đọc và viết từ tệp phụ trợ theo cách mà tôi không hiểu.

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

Các bài đọc và bài viết trong phần phụ trợ dường như bị chồng chéo, khiến mọi thứ bùng nổ.


2
Chúng ta đang nói chuyện lớn như thế nào? Yêu cầu của bạn buộc mọi thứ phải được giữ trong bộ nhớ. Giữ kết quả theo thứ tự có nghĩa là lệnh1 phải hoàn thành trước (vì vậy có lẽ nó đã đọc toàn bộ đầu vào và in toàn bộ đầu ra), trước khi lệnh2 và lệnh3 thậm chí có thể bắt đầu xử lý (trừ khi bạn cũng muốn thu thập đầu ra của chúng trong bộ nhớ).
frostschutz

bạn đã đúng, đầu vào và đầu ra của lệnh2 và lệnh3 quá lớn để giữ trong bộ nhớ. Tôi đã mong đợi sử dụng trao đổi sẽ hoạt động tốt hơn so với sử dụng các tập tin tạm thời. Một vấn đề khác tôi có là điều này xảy ra trong một vòng lặp, và điều đó làm cho việc xử lý các tệp thậm chí còn khó hơn. Tôi đang sử dụng một tệp duy nhất nhưng tại thời điểm này vì một số lý do, có một số sự trùng lặp trong việc đọc và viết từ tệp khiến tệp đó phát triển quảng cáo vô hạn. Tôi sẽ cố gắng cập nhật câu hỏi mà không làm bạn nhàm chán với quá nhiều chi tiết.
Trylks

4
Bạn phải sử dụng các tập tin tạm thời; hoặc cho đầu vào echo HelloWorld > file; (command1<file;command2<file;command3<file)hoặc cho đầu ra echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Đó chỉ là cách nó hoạt động - tee chỉ có thể rẽ nhánh nếu tất cả các lệnh hoạt động và xử lý song song. nếu một lệnh ngủ (vì bạn không muốn xen kẽ), nó sẽ đơn giản chặn tất cả các lệnh, để tránh lấp đầy bộ nhớ với đầu vào ...
frostschutz

Câu trả lời:


27

Bạn có thể sử dụng kết hợp GNU stdbuf và peetừ moreutils :

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

pee popen(3)s 3 dòng lệnh shell đó và sau đó freads đầu vào và fwrites cả ba dòng, sẽ được đệm đến 1M.

Ý tưởng là có một bộ đệm ít nhất là lớn như đầu vào. Theo cách này, mặc dù ba lệnh được khởi động cùng một lúc, chúng sẽ chỉ thấy đầu vào đến khi pee pcloses ba lệnh liên tục.

Sau mỗi lần pclose, hãy peexóa bộ đệm cho lệnh và chờ kết thúc. Điều đó đảm bảo rằng miễn là các cmdxlệnh đó không bắt đầu xuất bất cứ thứ gì trước khi chúng nhận được bất kỳ đầu vào nào (và không rẽ nhánh một quá trình có thể tiếp tục xuất ra sau khi cha mẹ chúng quay trở lại), đầu ra của ba lệnh sẽ không xen kẽ.

Trên thực tế, đó là một chút giống như sử dụng tệp tạm thời trong bộ nhớ, với nhược điểm là 3 lệnh được bắt đầu đồng thời.

Để tránh bắt đầu các lệnh đồng thời, bạn có thể viết peedưới dạng hàm shell:

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

Nhưng hãy cẩn thận, các shell khác với zshsẽ không thành công cho đầu vào nhị phân với các ký tự NUL.

Điều đó tránh sử dụng các tệp tạm thời, nhưng điều đó có nghĩa là toàn bộ đầu vào được lưu trữ trong bộ nhớ.

Trong mọi trường hợp, bạn sẽ phải lưu trữ đầu vào ở đâu đó, trong bộ nhớ hoặc tệp tạm thời.

Trên thực tế, đây là một câu hỏi khá thú vị, vì nó cho chúng ta thấy giới hạn của ý tưởng Unix về việc có một số công cụ đơn giản hợp tác với một nhiệm vụ duy nhất.

Ở đây, chúng tôi muốn có một số công cụ hợp tác với nhiệm vụ:

  • một lệnh nguồn (ở đây echo)
  • một lệnh điều phối ( tee)
  • một số lệnh lọc ( cmd1, cmd2, cmd3)
  • và một lệnh tổng hợp ( cat).

Sẽ thật tuyệt nếu tất cả họ có thể chạy cùng một lúc và làm việc chăm chỉ với dữ liệu mà họ dự định sẽ xử lý ngay khi có sẵn.

Trong trường hợp có một lệnh lọc, thật dễ dàng:

src | tee | cmd1 | cat

Tất cả các lệnh được chạy đồng thời, cmd1bắt đầu nhai dữ liệu srcngay khi có sẵn.

Bây giờ, với ba lệnh lọc, chúng ta vẫn có thể làm tương tự: khởi động chúng đồng thời và kết nối chúng với các đường ống:

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

Mà chúng ta có thể làm tương đối dễ dàng với các đường ống được đặt tên :

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'

(ở trên } 3<&0là để giải quyết vấn đề &chuyển hướng stdintừ /dev/nullvà chúng tôi sử dụng <>để tránh việc mở các đường ống để chặn cho đến khi đầu kia ( cat) cũng mở ra)

Hoặc để tránh các đường ống được đặt tên, đau đớn hơn một chút với 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'

Bây giờ, câu hỏi là: một khi tất cả các chương trình được khởi động và kết nối, liệu luồng dữ liệu sẽ?

Chúng tôi có hai điều trái ngược:

  • tee cung cấp cho tất cả các đầu ra của nó ở cùng một tốc độ, vì vậy nó chỉ có thể gửi dữ liệu ở tốc độ của ống đầu ra chậm nhất của nó.
  • cat sẽ chỉ bắt đầu đọc từ ống thứ hai (ống 6 trong bản vẽ trên) khi tất cả dữ liệu đã được đọc từ ống thứ nhất (5).

Điều đó có nghĩa là dữ liệu sẽ không chảy trong ống 6 cho đến khi cmd1kết thúc. Và, giống như trong trường hợp tr b Bở trên, điều đó có thể có nghĩa là dữ liệu sẽ không chảy trong ống 3, điều đó có nghĩa là nó sẽ không chảy trong bất kỳ ống 2, 3 hoặc 4 nào vì teethức ăn có tốc độ chậm nhất trong cả 3.

Trong thực tế, các đường ống đó có kích thước không rỗng, vì vậy một số dữ liệu sẽ quản lý để vượt qua và ít nhất trên hệ thống của tôi, tôi có thể làm cho nó hoạt động tới:

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

Ngoài ra, với

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

Chúng ta đã có một bế tắc, trong đó chúng ta đang ở trong tình huống này:

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

Chúng tôi đã lấp đầy các ống 3 và 6 (mỗi ống 64kiB). teeđã đọc thêm byte đó, nó đã đưa nó vào cmd1, nhưng

  • Bây giờ nó bị chặn viết trên ống 3 vì nó đang chờ để cmd2làm trống nó
  • cmd2không thể làm trống nó bởi vì nó bị chặn viết trên ống 6, chờ để catlàm trống nó
  • cat không thể làm trống nó bởi vì nó chờ cho đến khi không còn đầu vào trên ống 5.
  • cmd1không thể nói catkhông có thêm đầu vào vì nó đang chờ thêm đầu vào từ đó tee.
  • teekhông thể nói cmd1không có thêm đầu vào vì nó bị chặn ... vân vân.

Chúng ta đã có một vòng lặp phụ thuộc và do đó bế tắc.

Bây giờ, giải pháp là gì? Các ống lớn hơn 3 và 4 (đủ lớn để chứa tất cả srcđầu ra) sẽ làm điều đó. Chúng ta có thể làm điều đó chẳng hạn bằng cách chèn pv -qB 1Ggiữa teecmd2/3nơi pvcó thể lưu trữ tới 1G dữ liệu đang chờ cmd2cmd3đọc chúng. Điều đó có nghĩa là hai điều mặc dù:

  1. đó là sử dụng rất nhiều bộ nhớ và hơn thế nữa, sao chép nó
  2. điều đó không có cả 3 lệnh hợp tác vì cmd2trong thực tế sẽ chỉ bắt đầu xử lý dữ liệu khi cmd1 kết thúc.

Một giải pháp cho vấn đề thứ hai là làm cho ống 6 và 7 lớn hơn. Giả sử rằng cmd2cmd3tạo ra nhiều sản lượng như họ tiêu thụ, điều đó sẽ không tiêu tốn nhiều bộ nhớ hơn.

Cách duy nhất để tránh trùng lặp dữ liệu (trong vấn đề đầu tiên) là thực hiện việc lưu giữ dữ liệu trong chính bộ điều phối, đó là thực hiện một biến thể trên teeđó có thể cung cấp dữ liệu ở tốc độ đầu ra nhanh nhất (giữ dữ liệu để cung cấp dữ liệu những người chậm hơn ở tốc độ của riêng họ). Không thực sự tầm thường.

Vì vậy, cuối cùng, thứ tốt nhất chúng ta có thể có được mà không cần lập trình có lẽ là một cái gì đó giống như (cú pháp 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

Bạn nói đúng, bế tắc là vấn đề lớn nhất mà tôi đã tìm thấy cho đến nay để tránh sử dụng các tệp tạm thời. Các tệp này dường như khá nhanh, tuy nhiên, tôi không biết liệu chúng có được lưu trong bộ nhớ cache ở đâu đó không, tôi sợ thời gian truy cập đĩa, nhưng chúng có vẻ hợp lý cho đến nay.
Trylks

6
Một bổ sung +1 cho nghệ thuật ASCII tốt đẹp :-)
Kurt Pfeifle

3

Những gì bạn đề xuất không thể được thực hiện dễ dàng với bất kỳ lệnh hiện có nào và dù sao cũng không có ý nghĩa gì. Toàn bộ ý tưởng của ống ( |trong Unix / Linux) là trong cmd1 | cmd2các cmd1đầu ra viết (nhiều nhất) cho đến khi một lấp đầy bộ nhớ đệm, và sau đó cmd2chạy đọc dữ liệu từ bộ đệm (nhiều nhất) cho đến khi nó là trống rỗng. Tức là, cmd1cmd2chạy cùng một lúc, không bao giờ cần có nhiều hơn một lượng dữ liệu hạn chế "trong chuyến bay" giữa chúng. Nếu bạn muốn kết nối một số đầu vào với một đầu ra duy nhất, nếu một trong những độc giả tụt lại phía sau những đầu vào khác thì bạn sẽ dừng các đầu vào khác (điểm nào đang chạy song song?) Hoặc bạn bỏ đi đầu ra mà kẻ chậm trễ chưa đọc (điểm quan trọng của việc không có tệp trung gian là gì?). phức tạp hơn.

Trong gần 30 năm trải nghiệm Unix tôi không nhớ bất kỳ tình huống nào thực sự có lợi cho một đường ống đa đầu ra như vậy.

Bạn có thể kết hợp nhiều kết quả đầu ra thành một luồng ngay hôm nay, không theo bất kỳ cách xen kẽ nào (làm thế nào để đầu ra cmd1cmd2được xen kẽ? Một dòng lần lượt? Thay phiên nhau viết 10 byte? T viết bất cứ điều gì trong một thời gian dài? tất cả điều này là phức tạp để xử lý). Nó được thực hiện bởi, ví dụ (cmd1; cmd2; cmd3) | cmd4, các chương trình cmd1, cmd2cmd3được chạy lần lượt, đầu ra được gửi làm đầu vào cmd4.


3

Đối với vấn đề chồng chéo của bạn, trên Linux (và có bashhoặc zshkhông có ksh93), bạn có thể thực hiện như sau:

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
)

Lưu ý việc sử dụng (...)thay vì {...}để có được một quy trình mới ở mỗi lần lặp để chúng ta có thể có một fd 3 mới trỏ đến một quy trình mới auxfile. < /dev/fd/3là một mẹo để truy cập mà bây giờ đã xóa tập tin. Nó sẽ không hoạt động trên các hệ thống khác ngoài Linux < /dev/fd/3giống như dup2(3, 0)vậy và vì vậy fd 0 sẽ được mở ở chế độ chỉ ghi với con trỏ ở cuối tệp.

Để tránh ngã ba cho một số chức năng lồng nhau, bạn có thể viết nó dưới dạng:

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
}

Shell sẽ đảm nhiệm việc sao lưu fd 3 ở mỗi lần lặp. Cuối cùng, bạn sẽ hết bộ mô tả tập tin sớm hơn.

Mặc dù bạn sẽ thấy nó hiệu quả hơn khi làm điều đó như:

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

Đó là, đừng lồng các chuyển hướng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.