tee + cat: gunakan output beberapa kali dan kemudian gabungkan hasilnya

18

Jika saya memanggil beberapa perintah, misalnya echosaya dapat menggunakan hasil dari perintah itu di beberapa perintah lain dengan tee. Contoh:

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

Dengan kucing saya dapat mengumpulkan hasil dari beberapa perintah. Contoh:

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

Saya ingin dapat melakukan kedua hal pada saat yang sama, sehingga saya dapat menggunakan teeuntuk memanggil perintah-perintah itu pada output dari sesuatu yang lain (misalnya echosaya sudah menulis) dan kemudian mengumpulkan semua hasil mereka pada satu output dengan cat.

Sangat penting untuk menjaga hasil dalam urutan, ini berarti garis dalam output command1, command2dan command3tidak boleh terjalin, tetapi dipesan sesuai perintah (seperti yang terjadi dengan cat).

Mungkin ada opsi yang lebih baik daripada catdan teetetapi itu adalah yang saya tahu sejauh ini.

Saya ingin menghindari menggunakan file sementara karena ukuran input dan output mungkin besar.

Bagaimana saya bisa melakukan ini?

PD: masalah lain adalah bahwa ini terjadi dalam satu lingkaran, yang membuat penanganan file sementara lebih sulit. Ini adalah kode saat ini yang saya miliki dan bekerja untuk testcases kecil, tetapi ini menciptakan loop tak terbatas ketika membaca dan menulis dari auxfile dalam beberapa cara yang saya tidak mengerti.

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

Bacaan dan tulisan dalam auxfile tampaknya tumpang tindih, menyebabkan semuanya meledak.

Trylks
sumber
2
Seberapa besar kita berbicara? Persyaratan Anda memaksa semuanya disimpan dalam memori. Mempertahankan hasil secara berurutan berarti bahwa command1 harus menyelesaikan terlebih dahulu (sehingga mungkin membaca seluruh input dan mencetak seluruh output), sebelum command2 dan command3 bahkan dapat mulai memproses (kecuali jika Anda ingin mengumpulkan output mereka dalam memori pada awalnya juga).
frostschutz
Anda benar, input dan output dari command2 dan command3 terlalu besar untuk disimpan dalam memori. Saya berharap menggunakan swap akan bekerja lebih baik daripada menggunakan file sementara. Masalah lain yang saya miliki adalah bahwa ini terjadi dalam satu lingkaran, dan itu membuat penanganan file lebih sulit. Saya menggunakan satu file tetapi pada saat ini karena beberapa alasan ada beberapa tumpang tindih dalam membaca dan menulis dari file yang menyebabkannya tumbuh tak terhingga. Saya akan mencoba memperbarui pertanyaan tanpa membuat Anda bosan dengan terlalu banyak detail.
Trylks
4
Anda harus menggunakan file sementara; baik untuk input echo HelloWorld > file; (command1<file;command2<file;command3<file)atau untuk output echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Begitulah cara kerjanya - tee dapat memasukkan input hanya jika semua perintah bekerja dan memproses secara paralel. jika salah satu perintah tidur (karena Anda tidak ingin interleaving) itu hanya akan memblokir semua perintah, sehingga mencegah mengisi memori dengan input ...
frostschutz

Jawaban:

27

Anda dapat menggunakan kombinasi GNU stdbuf dan peedari moreutils :

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

kembangkan popen(3)3 baris perintah shell dan freads input dan fwrites ketiga, yang akan disangga hingga 1M.

Idenya adalah memiliki buffer setidaknya sebesar input. Dengan cara ini meskipun ketiga perintah dimulai pada saat yang sama, mereka hanya akan melihat input yang masuk ketika pee pcloseketiga perintah berurutan.

Setelah masing-masing pclose, peeflush buffer ke perintah dan menunggu penghentiannya. Itu menjamin bahwa selama cmdxperintah - perintah itu tidak mulai mengeluarkan apa pun sebelum mereka menerima masukan apa pun (dan jangan percabangan proses yang dapat terus menghasilkan setelah orangtua mereka kembali), keluaran dari ketiga perintah itu tidak akan disisipkan.

Efeknya, itu agak seperti menggunakan file temp di memori, dengan kelemahan bahwa 3 perintah dimulai bersamaan.

Untuk menghindari memulai perintah secara bersamaan, Anda bisa menulis peesebagai fungsi shell:

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

Namun berhati-hatilah bahwa cangkang selain zshakan gagal untuk input biner dengan karakter NUL.

Itu menghindari menggunakan file-file sementara, tetapi itu berarti seluruh input disimpan dalam memori.

Bagaimanapun, Anda harus menyimpan input di suatu tempat, dalam memori atau file temp.

Sebenarnya, ini adalah pertanyaan yang cukup menarik, karena ini menunjukkan kepada kita batas ide Unix untuk memiliki beberapa alat sederhana yang bekerja sama untuk satu tugas.

Di sini, kami ingin beberapa alat bekerja sama dengan tugas:

  • perintah sumber (di sini echo)
  • perintah dispatcher ( tee)
  • beberapa perintah Filter ( cmd1, cmd2, cmd3)
  • dan perintah agregasi ( cat).

Alangkah baiknya jika mereka semua bisa berjalan bersama pada saat yang sama dan melakukan kerja keras mereka pada data yang akan diproses setelah tersedia.

Dalam kasus satu perintah filter, mudah:

src | tee | cmd1 | cat

Semua perintah dijalankan secara bersamaan, cmd1mulai mengunyah data srcsegera setelah tersedia.

Sekarang, dengan tiga perintah filter, kita masih bisa melakukan hal yang sama: mulai secara bersamaan dan hubungkan dengan pipa:

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

Yang bisa kita lakukan relatif mudah dengan pipa bernama :

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'

(di atas } 3<&0adalah untuk bekerja di sekitar fakta bahwa &pengalihan stdindari /dev/null, dan kami gunakan <>untuk menghindari pembukaan pipa untuk memblokir sampai ujung lainnya ( cat) telah dibuka juga)

Atau untuk menghindari pipa bernama, sedikit lebih menyakitkan dengan 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'

Sekarang, pertanyaannya adalah: setelah semua program dimulai dan terhubung, akankah data mengalir?

Kami memiliki dua batasan:

  • tee mengumpankan semua outputnya pada kecepatan yang sama, sehingga hanya dapat mengirimkan data pada laju pipa keluaran yang paling lambat.
  • cat hanya akan mulai membaca dari pipa kedua (pipa 6 pada gambar di atas) ketika semua data telah dibaca dari yang pertama (5).

Artinya adalah bahwa data tidak akan mengalir di pipa 6 sampai cmd1selesai. Dan, seperti dalam kasus di tr b Batas, itu mungkin berarti bahwa data tidak akan mengalir di pipa 3 baik, yang berarti tidak akan mengalir di pipa 2, 3 atau 4 karena teefeed pada laju paling lambat dari semua 3.

Dalam praktiknya, pipa-pipa tersebut memiliki ukuran non-nol, sehingga beberapa data akan berhasil dilewati, dan setidaknya pada sistem saya, saya bisa membuatnya berfungsi hingga:

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

Di luar itu, dengan

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

Kita mengalami kebuntuan, di mana kita berada dalam situasi ini:

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

Kami telah mengisi pipa 3 dan 6 (masing-masing 64kiB). teetelah membaca byte tambahan itu, ia telah memasukkannya cmd1, tetapi

  • sekarang diblokir menulis di pipa 3 karena sedang menunggu untuk cmd2mengosongkannya
  • cmd2tidak dapat mengosongkannya karena itu diblokir menulis di pipa 6, menunggu untuk catmengosongkannya
  • cat tidak dapat mengosongkannya karena menunggu sampai tidak ada lagi input pada pipa 5.
  • cmd1tidak dapat memberi tahu catbahwa tidak ada lagi input karena menunggu sendiri untuk mendapat lebih banyak input tee.
  • dan teetidak tahu cmd1tidak ada input lagi karena diblokir ... dan seterusnya.

Kami memiliki loop ketergantungan dan dengan demikian jalan buntu.

Sekarang, apa solusinya? Pipa yang lebih besar 3 dan 4 (cukup besar untuk menampung semua srckeluaran) akan melakukannya. Kita bisa melakukannya misalnya dengan menyisipkan di pv -qB 1Gantara teedan di cmd2/3mana pvbisa menyimpan hingga 1G data menunggu cmd2dan cmd3membacanya. Itu berarti dua hal:

  1. yang berpotensi menggunakan banyak memori, dan lebih lagi, menduplikasinya
  2. itu gagal memiliki semua 3 perintah bekerja sama karena cmd2pada kenyataannya hanya akan mulai memproses data ketika cmd1 telah selesai.

Solusi untuk masalah kedua adalah membuat pipa 6 dan 7 lebih besar juga. Dengan asumsi cmd2dan cmd3menghasilkan output sebanyak yang mereka konsumsi, itu tidak akan menghabiskan lebih banyak memori.

Satu-satunya cara untuk menghindari duplikasi data (dalam masalah pertama) adalah dengan menerapkan retensi data dalam operator itu sendiri, yaitu menerapkan variasi pada teeyang dapat memberi makan data pada tingkat output tercepat (memegang data untuk memberi makan yang lebih lambat dengan langkah mereka sendiri). Tidak terlalu sepele.

Jadi, pada akhirnya, yang terbaik yang bisa kita dapatkan tanpa pemrograman mungkin adalah sesuatu seperti (sintaks 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
Stéphane Chazelas
sumber
Anda benar, kebuntuan adalah masalah terbesar yang saya temukan sejauh ini untuk menghindari menggunakan file sementara. File-file ini tampaknya cukup cepat, meskipun, saya tidak tahu apakah mereka sedang di-cache di suatu tempat, saya takut waktu akses disk, tetapi sejauh ini tampaknya masuk akal.
Trylks
6
Tambahan +1 untuk seni ASCII yang bagus :-)
Kurt Pfeifle
3

Apa yang Anda usulkan tidak dapat dilakukan dengan mudah dengan perintah yang ada, dan bagaimanapun juga tidak masuk akal. Seluruh ide dari pipa ( |di Unix / Linux) adalah bahwa dalam cmd1 | cmd2satu cmd1keluaran menulis (paling) sampai mengisi memori buffer, dan kemudian cmd2berjalan membaca data dari buffer (paling banyak) sampai kosong. Yaitu, cmd1dan cmd2berjalan pada saat yang sama, tidak pernah diperlukan untuk memiliki lebih dari jumlah data yang terbatas "dalam penerbangan" di antara mereka. Jika Anda ingin menghubungkan beberapa input ke satu output, jika salah satu pembaca tertinggal dari yang lain, Anda dapat menghentikan yang lain (apa gunanya berjalan secara paralel?) Atau Anda menyimpan output yang laggard belum baca (lalu apa gunanya tidak memiliki file perantara?). Lebih kompleks.

Dalam hampir 30 tahun pengalaman saya di Unix, saya tidak ingat situasi apa pun yang benar-benar akan menguntungkan pipa multi-output semacam itu.

Anda dapat menggabungkan beberapa output menjadi satu aliran hari ini, hanya saja tidak dengan cara yang saling terkait (bagaimana seharusnya output cmd1dan cmd2interleave? Satu baris pada gilirannya? Bergiliran menulis 10 byte? Alternatif "paragraf" didefinisikan entah bagaimana? Dan jika satu saja tidak t menulis sesuatu untuk waktu yang lama? semua ini rumit untuk ditangani). Ini dilakukan oleh, misalnya (cmd1; cmd2; cmd3) | cmd4, program cmd1,, cmd2dan cmd3dijalankan satu demi satu, hasilnya dikirim sebagai input cmd4.

vonbrand
sumber
3

Untuk masalah Anda yang tumpang tindih, di Linux (dan dengan bashatau zshtetapi tidak dengan ksh93), Anda dapat melakukannya sebagai:

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
)

Catat penggunaan (...)bukannya {...}untuk mendapatkan proses baru di setiap iterasi sehingga kita dapat memiliki fd 3 baru menunjuk ke yang baru auxfile. < /dev/fd/3adalah trik untuk mengakses file yang sekarang dihapus. Ini tidak akan bekerja pada sistem selain Linux di mana < /dev/fd/3suka dup2(3, 0)dan fd 0 akan terbuka dalam mode tulis-saja dengan kursor di akhir file.

Untuk menghindari percabangan fungsi yang bersarang, Anda dapat menuliskannya sebagai:

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 akan membantu mencadangkan fd 3 di setiap iterasi. Anda akhirnya kehabisan deskriptor file lebih cepat.

Meskipun Anda akan menemukan itu lebih efisien untuk melakukannya sebagai:

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

Artinya, jangan membuat sarang pengalihan.

Stéphane Chazelas
sumber