Anda dapat menggunakan kombinasi GNU stdbuf dan pee
dari moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
kembangkan popen(3)
3 baris perintah shell dan fread
s input dan fwrite
s 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
pclose
ketiga perintah berurutan.
Setelah masing-masing pclose
, pee
flush buffer ke perintah dan menunggu penghentiannya. Itu menjamin bahwa selama cmdx
perintah - 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 pee
sebagai 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 zsh
akan 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, cmd1
mulai mengunyah data src
segera 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<&0
adalah untuk bekerja di sekitar fakta bahwa &
pengalihan stdin
dari /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 zsh
coproc:
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 cmd1
selesai. Dan, seperti dalam kasus di tr b B
atas, itu mungkin berarti bahwa data tidak akan mengalir di pipa 3 baik, yang berarti tidak akan mengalir di pipa 2, 3 atau 4 karena tee
feed 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). tee
telah membaca byte tambahan itu, ia telah memasukkannya cmd1
, tetapi
- sekarang diblokir menulis di pipa 3 karena sedang menunggu untuk
cmd2
mengosongkannya
cmd2
tidak dapat mengosongkannya karena itu diblokir menulis di pipa 6, menunggu untuk cat
mengosongkannya
cat
tidak dapat mengosongkannya karena menunggu sampai tidak ada lagi input pada pipa 5.
cmd1
tidak dapat memberi tahu cat
bahwa tidak ada lagi input karena menunggu sendiri untuk mendapat lebih banyak input tee
.
- dan
tee
tidak tahu cmd1
tidak 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 src
keluaran) akan melakukannya. Kita bisa melakukannya misalnya dengan menyisipkan di pv -qB 1G
antara tee
dan di cmd2/3
mana pv
bisa menyimpan hingga 1G data menunggu cmd2
dan cmd3
membacanya. Itu berarti dua hal:
- yang berpotensi menggunakan banyak memori, dan lebih lagi, menduplikasinya
- itu gagal memiliki semua 3 perintah bekerja sama karena
cmd2
pada 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 cmd2
dan cmd3
menghasilkan 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 tee
yang 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