Bagaimana Anda menggunakan perintah coproc di berbagai shell?

Jawaban:

118

co-proses adalah kshfitur (sudah dalam ksh88). zshtelah memiliki fitur dari awal (awal 90-an), sementara itu baru saja ditambahkan bashpada 4.0(2009).

Namun, perilaku dan antarmuka berbeda secara signifikan antara 3 shell.

Idenya adalah sama, meskipun: memungkinkan untuk memulai pekerjaan di latar belakang dan dapat mengirim input dan membaca outputnya tanpa harus menggunakan pipa bernama.

Itu dilakukan dengan pipa tanpa nama dengan sebagian besar cangkang dan soket dengan versi terbaru dari ksh93 pada beberapa sistem.

Dalam a | cmd | b, amengumpankan data ke cmddan bmembaca hasilnya. Berjalan cmdsebagai proses bersama memungkinkan shell menjadi keduanya adan b.

ksh co-proses

Di ksh, Anda memulai coprocess sebagai:

cmd |&

Anda memberi makan data cmddengan melakukan hal-hal seperti:

echo test >&p

atau

print -p test

Dan baca cmdkeluaran dengan hal-hal seperti:

read var <&p

atau

read -p var

cmddimulai sebagai setiap pekerjaan latar belakang, Anda dapat menggunakan fg, bg, killdi atasnya dan merujuk dengan %job-numberatau melalui $!.

Untuk menutup ujung tulisan dari pipa cmdyang dibaca, Anda dapat melakukan:

exec 3>&p 3>&-

Dan untuk menutup ujung pembacaan dari pipa lain (yang cmdsedang menulis):

exec 3<&p 3<&-

Anda tidak dapat memulai proses bersama kedua kecuali Anda terlebih dahulu menyimpan deskriptor file pipa ke beberapa fds lainnya. Misalnya:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

zsh proses bersama

Dalam zsh, co-proses hampir identik dengan yang ada di ksh. Satu-satunya perbedaan nyata adalah bahwa zshproses bersama dimulai dengan coprockata kunci.

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

Perbuatan:

exec 3>&p

Catatan: Ini tidak memindahkan coprocdeskriptor file ke fd 3(seperti di ksh), tetapi menduplikatnya. Jadi, tidak ada cara eksplisit untuk menutup pipa makan atau membaca, selain memulai yang lain coproc .

Sebagai contoh, untuk menutup makan akhir:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc

Selain co-proses berbasis pipa, zsh(sejak 3.1.6-dev19, dirilis pada tahun 2000) memiliki konstruksi berbasis pseudo-tty seperti expect. Untuk berinteraksi dengan sebagian besar program, co-proses gaya ksh tidak akan berfungsi, karena program mulai melakukan buffering ketika hasilnya berupa pipa.

Berikut ini beberapa contohnya.

Mulai proses bersama x:

zmodload zsh/zpty
zpty x cmd

(Di sini, cmdadalah perintah sederhana. Tetapi Anda dapat melakukan hal-hal yang lebih menarik evalatau fungsi.)

Umpan data proses bersama:

zpty -w x some data

Baca data proses bersama (dalam kasus paling sederhana):

zpty -r x var

Seperti expect, itu bisa menunggu beberapa output dari co-proses yang cocok dengan pola yang diberikan.

bash co-proses

Sintaks bash jauh lebih baru, dan dibangun di atas fitur baru yang baru ditambahkan ke ksh93, bash, dan zsh. Ini memberikan sintaks untuk memungkinkan penanganan deskriptor file yang dialokasikan secara dinamis di atas 10.

bashmenawarkan sintaks dasar coproc , dan yang diperluas .

Sintaks dasar

Sintaks dasar untuk memulai proses-co terlihat seperti zsh:

coproc cmd

Di kshatau zsh, pipa ke dan dari co-proses diakses dengan >&pdan <&p.

Tetapi dalam bash, file deskriptor dari pipa dari co-proses dan pipa lainnya ke co-proses dikembalikan dalam $COPROCarray (masing ${COPROC[0]}- masing dan ${COPROC[1]}. Jadi ...

Umpan data ke proses bersama:

echo xxx >&"${COPROC[1]}"

Baca data dari proses bersama:

read var <&"${COPROC[0]}"

Dengan sintaks dasar, Anda hanya dapat memulai satu proses bersama pada saat itu.

Sintaks yang diperluas

Dalam sintaks yang diperluas, Anda dapat memberi nama proses-bersama Anda (seperti dalam zshproses-proses bersama zpty):

coproc mycoproc { cmd; }

Perintah itu harus berupa perintah majemuk. (Perhatikan bagaimana contoh di atas mengingatkan function f { ...; }.)

Kali ini, deskriptor file dalam ${mycoproc[0]}dan ${mycoproc[1]}.

Anda dapat mulai lebih dari satu co-proses pada waktu-tetapi Anda lakukan mendapatkan peringatan ketika Anda mulai co-proses sementara satu masih berjalan (bahkan dalam mode non-interaktif).

Anda dapat menutup deskriptor file saat menggunakan sintaks yang diperluas.

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"

Perhatikan bahwa penutupan dengan cara itu tidak berfungsi dalam versi bash sebelum 4.3 di mana Anda harus menulisnya:

fd=${tr[1]}
exec {fd}>&-

Seperti pada kshdan zsh, deskriptor file pipa tersebut ditandai sebagai close-on-exec.

Namun dalam bash, satu-satunya cara untuk lulus mereka perintah dieksekusi adalah untuk menduplikasi mereka untuk fds 0, 1atau 2. Itu membatasi jumlah co-proses yang dapat Anda berinteraksi dengan untuk satu perintah. (Lihat contoh di bawah ini.)

proses yash dan pengalihan pipa

yashtidak memiliki fitur co-proses per se, tetapi konsep yang sama dapat diimplementasikan dengan pipeline dan fitur pengalihan proses . yashmemiliki antarmuka untuk pipe()panggilan sistem, sehingga hal semacam ini dapat dilakukan dengan relatif mudah dengan tangan di sana.

Anda akan memulai proses bersama dengan:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

Yang pertama menciptakan a pipe(4,5)(5 ujung penulisan, 4 ujung pembacaan), kemudian mengarahkan fd 3 ke pipa ke proses yang berjalan dengan stdin di ujung yang lain, dan stdout pergi ke pipa yang dibuat sebelumnya. Kemudian kita menutup ujung tulisan pipa itu di induk yang tidak kita perlukan. Jadi sekarang di shell kita memiliki fd 3 terhubung ke stdin cmd dan fd 4 terhubung ke stdout cmd dengan pipa.

Perhatikan bahwa flag close-on-exec tidak diatur pada deskriptor file tersebut.

Untuk memberi makan data:

echo data >&3 4<&-

Untuk membaca data:

read var <&4 3>&-

Dan Anda dapat menutup fds seperti biasa:

exec 3>&- 4<&-

Sekarang, mengapa mereka tidak begitu populer

hampir tidak ada untungnya menggunakan pipa bernama

Proses bersama dapat dengan mudah diimplementasikan dengan pipa bernama standar. Saya tidak tahu kapan tepatnya pipa bernama diperkenalkan tetapi mungkin itu setelah kshmuncul dengan proses bersama (mungkin pada pertengahan 80-an, ksh88 "dirilis" pada tahun 88, tapi saya percaya kshdigunakan secara internal di AT&T beberapa tahun sebelumnya itu) yang akan menjelaskan mengapa.

cmd |&
echo data >&p
read var <&p

Dapat ditulis dengan:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

Berinteraksi dengan mereka lebih mudah — terutama jika Anda perlu menjalankan lebih dari satu proses bersama. (Lihat contoh di bawah.)

Satu-satunya manfaat menggunakan coprocadalah Anda tidak perlu membersihkan pipa-pipa tersebut setelah digunakan.

jalan buntu

Kerang menggunakan pipa dalam beberapa konstruksi:

  • pipa shell: cmd1 | cmd2 ,
  • perintah substitusi: $(cmd) ,
  • dan proses substitusi: <(cmd) , >(cmd).

Dalam hal itu, data mengalir hanya dalam satu arah antara berbagai proses.

Namun, dengan proses bersama dan pipa bernama, mudah mengalami kebuntuan. Anda harus melacak perintah yang memiliki deskriptor file mana yang terbuka, untuk mencegahnya tetap terbuka dan menahan proses tetap hidup. Kebuntuan dapat menjadi sulit untuk diselidiki, karena mereka mungkin terjadi secara non-deterministik; misalnya, hanya ketika data sebanyak satu pipa diisi dikirim.

bekerja lebih buruk daripada expectuntuk apa itu dirancang untuk

Tujuan utama dari co-proses adalah untuk memberikan shell dengan cara untuk berinteraksi dengan perintah. Namun, itu tidak berfungsi dengan baik.

Bentuk kebuntuan paling sederhana yang disebutkan di atas adalah:

tr a b |&
echo a >&p
read var<&p

Karena outputnya tidak masuk ke terminal, trbuffer outputnya. Jadi ia tidak akan mengeluarkan apa-apa sampai ia melihat end-of-file-nya stdin, atau ia telah mengumpulkan buffer-penuh data ke output. Jadi di atas, setelah shell memiliki output a\n(hanya 2 byte), readakan memblokir tanpa batas karena trmenunggu shell untuk mengirimkan lebih banyak data.

Singkatnya, pipa tidak baik untuk berinteraksi dengan perintah. Co-proses hanya dapat digunakan untuk berinteraksi dengan perintah yang tidak buffer output mereka, atau perintah yang bisa dikatakan tidak buffer output mereka; misalnya, dengan menggunakan stdbufbeberapa perintah pada sistem GNU atau FreeBSD terbaru.

Itu sebabnya expectatau zptygunakan pseudo-terminal saja. expectadalah alat yang dirancang untuk berinteraksi dengan perintah, dan itu melakukannya dengan baik.

Penanganan deskriptor file adalah fiddly, dan sulit untuk diperbaiki

Co-proses dapat digunakan untuk melakukan beberapa pipa yang lebih kompleks daripada yang diizinkan oleh pipa shell sederhana.

bahwa jawaban Unix.SE lainnya memiliki contoh penggunaan coproc.

Berikut adalah contoh yang disederhanakan: Bayangkan Anda ingin fungsi yang memberi makan salinan output perintah ke 3 perintah lain, dan kemudian output dari 3 perintah tersebut digabungkan.

Semua menggunakan pipa.

Misalnya: memberi makan output dari printf '%s\n' foo barke tr a b, sed 's/./&&/g'dan cut -b2-untuk mendapatkan sesuatu seperti:

foo
bbr
ffoooo
bbaarr
oo
ar

Pertama, itu belum tentu jelas, tetapi ada kemungkinan kebuntuan di sana, dan itu akan mulai terjadi setelah hanya beberapa kilobyte data.

Kemudian, tergantung pada shell Anda, Anda akan menjalankan sejumlah masalah berbeda yang harus ditangani secara berbeda.

Misalnya, dengan zsh, Anda akan melakukannya dengan:

f() (
  coproc tr a b
  exec {o1}<&p {i1}>&p
  coproc sed 's/./&&/g' {i1}>&- {o1}<&-
  exec {o2}<&p {i2}>&p
  coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
  tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
  exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f

Di atas, co-proses fds memiliki set flag close-on-exec, tetapi bukan flag yang diduplikasi dari mereka (seperti pada {o1}<&p). Jadi, untuk menghindari kebuntuan, Anda harus memastikan mereka tutup dalam proses apa pun yang tidak membutuhkannya.

Demikian pula, kita harus menggunakan subkulit dan menggunakan exec catpada akhirnya, untuk memastikan tidak ada proses shell berbohong tentang memegang pipa terbuka.

Dengan ksh(di sini ksh93), itu harus:

f() (
  tr a b |&
  exec {o1}<&p {i1}>&p
  sed 's/./&&/g' |&
  exec {o2}<&p {i2}>&p
  cut -c2- |&
  exec {o3}<&p {i3}>&p
  eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
  eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f

( Catatan: Itu tidak akan berfungsi pada sistem yang kshmenggunakan socketpairsalih-alih pipes, dan di mana /dev/fd/nberfungsi seperti di Linux.)

Dalam ksh, fds di atas 2ditandai dengan flag close-on-exec, kecuali jika dilewatkan secara eksplisit pada baris perintah. Itu sebabnya kita tidak harus menutup deskriptor file yang tidak digunakan seperti dengan zsh—tapi itu juga mengapa kita harus melakukan {i1}>&$i1dan menggunakan evaluntuk nilai baru $i1, untuk diteruskan ke teedan cat...

Dalam bashhal ini tidak dapat dilakukan, karena Anda tidak dapat menghindari flag close-on-exec.

Di atas, ini relatif sederhana, karena kami hanya menggunakan perintah eksternal sederhana. Semakin rumit ketika Anda ingin menggunakan konstruksi shell di sana, dan Anda mulai mengalami bug shell.

Bandingkan yang di atas dengan yang sama menggunakan pipa bernama:

f() {
  mkfifo p{i,o}{1,2,3}
  tr a b < pi1 > po1 &
  sed 's/./&&/g' < pi2 > po2 &
  cut -c2- < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f

Kesimpulan

Jika Anda ingin berinteraksi dengan perintah, penggunaan expect, atau zsh's zpty, atau bernama pipa.

Jika Anda ingin melakukan pemipaan mewah dengan pipa, gunakan pipa bernama.

Co-proses dapat melakukan beberapa hal di atas, tetapi bersiaplah untuk melakukan beberapa goresan kepala serius untuk apa pun yang tidak sepele.

Stéphane Chazelas
sumber
Jawaban yang bagus. Saya tidak tahu kapan secara spesifik itu diperbaiki, tetapi setidaknya bash 4.3.11, Anda sekarang dapat menutup deskriptor file coproc secara langsung, tanpa perlu sebuah aux. variabel; dalam hal contoh dalam jawaban Anda exec {tr[1]}<&- sekarang akan berfungsi (untuk menutup stdin coproc; perhatikan bahwa kode Anda (secara tidak langsung) mencoba untuk menutup {tr[1]}menggunakan >&-, tetapi stdin{tr[1]} coproc , dan harus ditutup dengan ). Perbaikan pasti datang di suatu tempat antara , yang masih menunjukkan masalah, dan , yang tidak. <&-4.2.254.3.11
mklement0
1
@ mklement0, terima kasih. exec {tr[1]}>&-memang tampaknya bekerja dengan versi yang lebih baru dan direferensikan dalam entri CWRU / changelog ( izinkan kata-kata seperti {array [ind]} menjadi pengalihan yang valid ... 2012-09-01). exec {tr[1]}<&-(atau lebih tepatnya >&-setara meskipun itu tidak ada bedanya karena hanya membutuhkan close()keduanya) tidak menutup stdin coproc, tetapi akhir penulisan pipa ke coproc itu.
Stéphane Chazelas
1
@ mklement0, poin bagus, saya telah memperbaruinya dan menambahkan yash.
Stéphane Chazelas
1
Satu keuntungan lebih mkfifoadalah Anda tidak perlu khawatir tentang kondisi balapan dan keamanan untuk akses pipa. Anda masih harus khawatir tentang kebuntuan dengan fifo.
Otheus
1
Tentang deadlock: stdbufperintah dapat membantu mencegah setidaknya beberapa dari mereka. Saya menggunakannya di Linux dan bash. Pokoknya saya percaya @ StéphaneChazelas benar dalam Kesimpulan: fase "menggaruk kepala" berakhir untuk saya hanya ketika saya kembali ke pipa bernama.
shub
7

Proses bersama pertama kali diperkenalkan dalam bahasa scripting shell dengan ksh88shell (1988), dan kemudian di zshbeberapa titik sebelum 1993.

Sintaks untuk meluncurkan co-proses di bawah ksh adalah command |&. Mulai dari sana, Anda dapat menulis ke commandinput standar dengan print -pdan membaca output standarnya read -p.

Lebih dari beberapa dekade kemudian, bash yang kurang memiliki fitur ini akhirnya memperkenalkannya pada rilis 4.0-nya. Sayangnya, sintaks yang tidak kompatibel dan lebih kompleks dipilih.

Di bawah bash 4.0 dan yang lebih baru, Anda dapat meluncurkan proses bersama dengan coprocperintah, misalnya:

$ coproc awk '{print $2;fflush();}'

Anda kemudian dapat mengirimkan sesuatu ke perintah stdin seperti itu:

$ echo one two three >&${COPROC[1]}

dan baca output awk dengan:

$ read -ru ${COPROC[0]} foo
$ echo $foo
two

Di bawah ksh, itu akan menjadi:

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
Jlliagre
sumber
-1

Apa itu "coproc"?

Ini adalah kependekan dari "co-proses" yang berarti proses kedua bekerja sama dengan shell. Ini sangat mirip dengan pekerjaan latar belakang yang dimulai dengan "&" di akhir perintah, kecuali bahwa alih-alih berbagi input dan output standar yang sama dengan shell induknya, I / O standarnya terhubung ke shell induk dengan spesial jenis pipa yang disebut FIFO. Untuk referensi klik di sini

Seseorang memulai coproc di zsh dengan

coproc command

Perintah harus disiapkan untuk membaca dari stdin dan / atau menulis ke stdout, atau tidak banyak digunakan sebagai coproc.

Baca artikel ini di sini menyediakan studi kasus antara exec dan coproc

Munai Das Udasin
sumber
Bisakah Anda menambahkan beberapa artikel ke jawaban Anda? Saya mencoba untuk membahas topik ini di U&L karena sepertinya tidak terwakili. Terima kasih atas jawaban anda! Juga perhatikan saya menetapkan tag sebagai Bash, bukan zsh.
slm
@slm Anda sudah menunjuk ke peretas Bash. Saya melihat ada cukup contoh. Jika perhatian Anda adalah untuk membawa pertanyaan ini di bawah perhatian maka ya Anda berhasil:>
Valentin Bajrami
Mereka bukan jenis pipa khusus, mereka adalah pipa yang sama seperti yang digunakan |. (yaitu menggunakan pipa di sebagian besar kulit, dan soket di ksh93). pipa dan soket pasang adalah yang pertama masuk, pertama keluar, mereka semua FIFO. mkfifomembuat pipa bernama, proses-proses tidak menggunakan pipa bernama.
Stéphane Chazelas
@slm maaf untuk zsh ... sebenarnya saya bekerja di zsh. Saya cenderung melakukannya kadang-kadang dengan arus. Ini bekerja dengan baik di Bash juga ...
Munai Das Udasin
@ Stephane Chazelas Saya cukup yakin bahwa saya membacanya di suatu tempat bahwa itu I / O terhubung dengan jenis pipa khusus yang disebut FIFO ...
Munai Das Udasin
-1

Berikut ini contoh lain yang bagus (dan berfungsi) - server sederhana yang ditulis dalam BASH. Harap dicatat bahwa Anda akan membutuhkan OpenBSD netcat, yang klasik tidak akan berfungsi. Tentu saja Anda dapat menggunakan soket inet alih-alih unix satu.

server.sh:

#!/usr/bin/env bash

SOCKET=server.sock
PIDFILE=server.pid

(
    exec </dev/null
    exec >/dev/null
    exec 2>/dev/null
    coproc SERVER {
        exec nc -l -k -U $SOCKET
    }
    echo $SERVER_PID > $PIDFILE
    {
        while read ; do
            echo "pong $REPLY"
        done
    } <&${SERVER[0]} >&${SERVER[1]}
    rm -f $PIDFILE
    rm -f $SOCKET
) &
disown $!

client.sh:

#!/usr/bin/env bash

SOCKET=server.sock

coproc CLIENT {
    exec nc -U $SOCKET
}

{
    echo "$@"
    read
} <&${CLIENT[0]} >&${CLIENT[1]}

echo $REPLY

Pemakaian:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
Alexey Naidyonov
sumber