Qualcuno può fornire un paio di esempi su come utilizzare coproc
?
Risposta accettata:
i co-processi sono un ksh
funzione (già in ksh88
). zsh
ha avuto la funzione dall'inizio (primi anni '90), mentre è stata appena aggiunta solo a bash
in 4.0
(2009).
Tuttavia, il comportamento e l'interfaccia sono significativamente diversi tra le 3 shell.
L'idea, però, è la stessa:permette di avviare un lavoro in background e di poterlo inviare in input e leggerne l'output senza dover ricorrere a named pipe.
Questo viene fatto con pipe senza nome con la maggior parte delle shell e delle coppie di prese con versioni recenti di ksh93 su alcuni sistemi.
In a | cmd | b
, a
invia i dati a cmd
e b
ne legge l'output. Esecuzione di cmd
poiché un co-processo consente alla shell di essere sia a
e b
.
ksh co-elabora
In ksh
, avvii un coprocesso come:
cmd |&
Invii i dati a cmd
facendo cose come:
echo test >&p
o
print -p test
E leggi cmd
's output con cose come:
read var <&p
o
read -p var
cmd
viene avviato come qualsiasi lavoro in background, puoi usare fg
, bg
, kill
su di esso e referenziarlo con %job-number
o tramite $!
.
Per chiudere l'estremità di scrittura della pipe cmd
sta leggendo, puoi fare:
exec 3>&p 3>&-
E per chiudere l'estremità di lettura dell'altra pipe (quella cmd
scrive a):
exec 3<&p 3<&-
Non è possibile avviare un secondo coprocesso a meno che non si salvino prima i descrittori del file pipe in altri fd. Ad esempio:
tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
zsh coelabora
In zsh
, i coprocessi sono quasi identici a quelli in ksh
. L'unica vera differenza è che zsh
i co-processi vengono avviati con coproc
parola chiave.
coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
Fare:
exec 3>&p
Nota:questo non sposta il coproc
descrittore di file in fd 3
(come in ksh
), ma lo duplica. Quindi, non esiste un modo esplicito per chiudere il tubo di alimentazione o di lettura, altri iniziando un altro coproc
.
Ad esempio, per chiudere l'estremità di alimentazione:
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
Oltre ai coprocessi basati su pipe, zsh
(dal 3.1.6-dev19, rilasciato nel 2000) ha costrutti basati su pseudo-tty come expect
. Per interagire con la maggior parte dei programmi, i coprocessi in stile ksh non funzioneranno, poiché i programmi iniziano il buffering quando il loro output è una pipe.
Ecco alcuni esempi.
Avvia il co-processo x
:
zmodload zsh/zpty
zpty x cmd
(Qui, cmd
è un semplice comando Ma puoi fare cose più fantasiose con eval
o funzioni.)
Inserisci un coprocesso di dati:
zpty -w x some data
Leggi i dati di co-elaborazione (nel caso più semplice):
zpty -r x var
Come expect
, può attendere che l'output del coprocesso corrisponda a un determinato modello.
coprocessi bash
La sintassi bash è molto più recente e si basa su una nuova funzionalità aggiunta di recente a ksh93, bash e zsh. Fornisce una sintassi per consentire la gestione dei descrittori di file allocati dinamicamente superiori a 10.
bash
offre una base coproc
sintassi e un esteso uno.
Sintassi di base
La sintassi di base per avviare un coprocesso è zsh
's:
coproc cmd
In ksh
o zsh
, si accede alle pipe da e verso il co-processo con >&p
e <&p
.
Ma in bash
, i descrittori di file della pipe dal coprocesso e dall'altra pipe al coprocesso vengono restituiti nel $COPROC
array (rispettivamente ${COPROC[0]}
e ${COPROC[1]}
. Allora...
Invia i dati al coprocessore:
echo xxx >&"${COPROC[1]}"
Leggi i dati dal co-processo:
read var <&"${COPROC[0]}"
Con la sintassi di base, puoi avviare un solo coprocesso alla volta.
Sintassi estesa
Nella sintassi estesa, puoi nominare i tuoi coprocessi (come in zsh
zpty co-processi):
coproc mycoproc { cmd; }
Il comando ha essere un comando composto. (Nota come l'esempio sopra ricorda function f { ...; }
.)
Questa volta, i descrittori di file sono in ${mycoproc[0]}
e ${mycoproc[1]}
.
Puoi avviare più di un coprocesso alla volta, ma fai ricevi un avviso quando avvii un coprocesso mentre uno è ancora in esecuzione (anche in modalità non interattiva).
Puoi chiudere i descrittori di file quando usi la sintassi estesa.
coproc tr { tr a b; }
echo aaa >&"${tr[1]}"
exec {tr[1]}>&-
cat <&"${tr[0]}"
Nota che la chiusura in questo modo non funziona nelle versioni bash precedenti alla 4.3 dove devi invece scriverla:
fd=${tr[1]}
exec {fd}>&-
Come in ksh
e zsh
, quei descrittori di file pipe sono contrassegnati come close-on-exec.
Ma in bash
, l'unico modo per passarli ai comandi eseguiti è duplicarli in fds ,
1
o 2
. Ciò limita il numero di coprocessi con cui puoi interagire per un singolo comando. (Vedi sotto per un esempio.)
Processo di yash e reindirizzamento della pipeline
yash
non ha una funzione di co-processing di per sé, ma lo stesso concetto può essere implementato con la sua pipeline e processo funzionalità di reindirizzamento. yash
ha un'interfaccia per pipe()
chiamata di sistema, quindi questo genere di cose può essere fatto a mano in modo relativamente semplice.
Inizierai un co-processo con:
exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
Che prima crea una pipe(4,5)
(5 l'estremità di scrittura, 4 l'estremità di lettura), quindi reindirizza fd 3 a una pipe a un processo che viene eseguito con il suo stdin all'altra estremità e stdout va alla pipe creata in precedenza. Quindi chiudiamo l'estremità di scrittura di quella pipe nel genitore di cui non avremo bisogno. Quindi ora nella shell abbiamo fd 3 collegato allo stdin di cmd e fd 4 collegato allo stdout di cmd con pipe.
Nota che il flag close-on-exec non è impostato su quei descrittori di file.
Per alimentare i dati:
echo data >&3 4<&-
Per leggere i dati:
read var <&4 3>&-
E puoi chiudere gli fd come al solito:
exec 3>&- 4<&-
Ora, perché non sono così popolari
quasi alcun vantaggio rispetto all'utilizzo di named pipe
I co-processi possono essere facilmente implementati con named pipe standard. Non so quando siano state introdotte esattamente le pipe con nome, ma è possibile che fosse dopo ksh
ha inventato co-processi (probabilmente a metà degli anni '80, ksh88 è stato "rilasciato" nell'88, ma credo che ksh
è stato utilizzato internamente ad AT&T alcuni anni prima) il che spiegherebbe il perché.
cmd |&
echo data >&p
read var <&p
Può essere scritto con:
mkfifo in out
cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
Interagire con quelli è più semplice, soprattutto se è necessario eseguire più di un coprocesso. (Vedi esempi sotto.)
L'unico vantaggio dell'utilizzo di coproc
è che non devi ripulire quelle pipe con nome dopo l'uso.
a rischio di stallo
Le shell usano pipe in alcuni costrutti:
- tubi a conchiglia:
cmd1 | cmd2
, - sostituzione comando:
$(cmd)
, - e sostituzione del processo:
<(cmd)
,>(cmd)
.
In questi, i dati fluiscono in solo uno direzione tra diversi processi.
Con i co-processi e le named pipe, tuttavia, è facile imbattersi in un deadlock. Devi tenere traccia di quale comando ha quale descrittore di file aperto, per evitare che uno rimanga aperto e mantenga vivo un processo. I deadlock possono essere difficili da indagare, perché possono verificarsi in modo non deterministico; ad esempio, solo quando vengono inviati tanti dati per riempire un tubo.
funziona peggio di expect
per quello per cui è stato progettato
Lo scopo principale dei co-processi era fornire alla shell un modo per interagire con i comandi. Tuttavia, non funziona così bene.
La forma più semplice di deadlock menzionata sopra è:
tr a b |&
echo a >&p
read var<&p
Poiché il suo output non va a un terminale, tr
bufferizza il suo output. Quindi non produrrà nulla finché non vedrà la fine del file sul suo stdin
o ha accumulato un buffer pieno di dati da produrre. Così sopra, dopo che la shell ha prodotto an
(solo 2 byte), il read
si bloccherà a tempo indeterminato perché tr
sta aspettando che la shell gli invii più dati.
In breve, le pipe non sono buone per interagire con i comandi. I coprocessi possono essere utilizzati solo per interagire con comandi che non memorizzano nel buffer il loro output, o comandi a cui si può dire di non bufferizzare il loro output; ad esempio, utilizzando stdbuf
con alcuni comandi sui recenti sistemi GNU o FreeBSD.
Ecco perché expect
o zpty
usa invece pseudo-terminali. expect
è uno strumento progettato per interagire con i comandi e lo fa bene.
La gestione del descrittore di file è complicata e difficile da correggere
I co-processi possono essere utilizzati per eseguire impianti idraulici più complessi di quelli consentiti dai semplici tubi a conchiglia.
quell'altra risposta Unix.SE ha un esempio di utilizzo di coproc.
Ecco un esempio semplificato: Immagina di volere una funzione che invii una copia dell'output di un comando ad altri 3 comandi, e quindi l'output di quei 3 comandi venga concatenato.
Tutti con tubi.
Ad esempio:alimenta l'output di printf '%sn' foo bar
a tr a b
, sed 's/./&&/g'
e cut -b2-
per ottenere qualcosa come:
foo
bbr
ffoooo
bbaarr
oo
ar
Innanzitutto, non è necessariamente ovvio, ma c'è la possibilità di un deadlock lì e inizierà a verificarsi dopo solo pochi kilobyte di dati.
Quindi, a seconda della tua shell, ti imbatterai in una serie di problemi diversi che devono essere affrontati in modo diverso.
Correlati:come viene interpretato il carattere jolly * come comando?
Ad esempio, con zsh
, lo faresti con:
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 '%sn' foo bar | f
Sopra, gli fd co-process hanno il flag close-on-exec impostato, ma non quelli che sono duplicati da loro (come in {o1}<&p
). Quindi, per evitare deadlock, dovrai assicurarti che siano chiusi in tutti i processi che non ne hanno bisogno.
Allo stesso modo, dobbiamo usare una subshell e usare exec cat
alla fine, per garantire che non ci sia alcun processo di shell che mente sul tenere aperta una pipa.
Con ksh
(qui ksh93
), dovrebbe essere:
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 '%sn' foo bar | f
(Nota: Non funzionerà su sistemi in cui ksh
utilizza socketpairs
invece di pipes
e dove /dev/fd/n
funziona come su Linux.)
In ksh
, fd sopra 2
sono contrassegnati con il flag close-on-exec, a meno che non vengano passati esplicitamente sulla riga di comando. Ecco perché non dobbiamo chiudere i descrittori di file inutilizzati come con zsh
—ma è anche il motivo per cui dobbiamo fare {i1}>&$i1
e usa eval
per quel nuovo valore di $i1
, da passare a tee
e cat
…
In bash
questo non può essere fatto, perché non puoi evitare il flag close-on-exec.
Sopra, è relativamente semplice, perché utilizziamo solo semplici comandi esterni. Diventa più complicato quando invece vuoi usare i costrutti della shell lì dentro e inizi a incappare in bug della shell.
Confronta quanto sopra con lo stesso usando le pipe con nome:
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 '%sn' foo bar | f
Conclusione
Se vuoi interagire con un comando, usa expect
o zsh
's zpty
o pipe con nome.
Se vuoi realizzare un impianto idraulico di fantasia con i tubi, usa i tubi con nome.
I co-processi possono eseguire alcune delle operazioni precedenti, ma preparati a grattarti la testa per qualsiasi cosa non banale.