Quelqu'un peut-il fournir quelques exemples sur la façon d'utiliser coproc
?
les co-processus sont une fonctionnalité ksh
(déjà dans ksh88
). zsh
a la fonctionnalité depuis le début (début des années 90), alors qu'elle vient juste d'être ajoutée à bash
dans 4.0
(2009).
Cependant, le comportement et l'interface sont significativement différents entre les 3 coques.
L'idée est la même, cependant: elle permet de démarrer un travail en arrière-plan et de pouvoir lui envoyer des entrées et lire ses sorties sans avoir à recourir aux canaux nommés.
Cela se fait avec des tuyaux sans nom avec la plupart des shells et des paires de prises avec des versions récentes de ksh93 sur certains systèmes.
Dans a | cmd | b
, a
transmet les données à cmd
et b
lit sa sortie. L'exécution de cmd
en tant que co-processus permet au shell d'être à la fois a
et b
.
Dans ksh
, vous démarrez un coprocessus en tant que:
cmd |&
Vous alimentez des données vers cmd
en faisant des choses comme:
echo test >&p
ou
print -p test
Et lisez la sortie de cmd
avec des choses comme:
read var <&p
ou
read -p var
cmd
est démarré comme n'importe quel travail d'arrière-plan, vous pouvez utiliser fg
, bg
, kill
dessus et le référencer par %job-number
ou via $!
.
Pour fermer l'extrémité d'écriture du tube à partir duquel cmd
lit, vous pouvez faire:
exec 3>&p 3>&-
Et pour fermer l'extrémité de lecture de l'autre pipe (celle vers laquelle cmd
écrit):
exec 3<&p 3<&-
Vous ne pouvez pas démarrer un deuxième co-processus à moins d'enregistrer d'abord les descripteurs de fichier de canal dans d'autres fds. Par exemple:
tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
Dans zsh
, les co-processus sont presque identiques à ceux dans ksh
. La seule vraie différence est que les coprocessus zsh
sont démarrés avec le mot clé coproc
.
coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
Faire:
exec 3>&p
Remarque: Cela ne déplace pas le descripteur de fichier coproc
vers fd 3
(Comme dans ksh
), mais le duplique. Il n'y a donc aucun moyen explicite de fermer le tuyau d'alimentation ou de lecture, autre commençant un autrecoproc
.
Par exemple, pour fermer l'extrémité d'alimentation:
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
En plus des co-processus basés sur des tuyaux, zsh
(depuis 3.1.6-dev19, publié en 2000) a des constructions basées sur des pseudo-tty comme expect
. Pour interagir avec la plupart des programmes, les co-processus de style ksh ne fonctionneront pas, car les programmes commencent à mettre en mémoire tampon lorsque leur sortie est un canal.
Voici quelques exemples.
Démarrer le co-processus x
:
zmodload zsh/zpty
zpty x cmd
(Ici, cmd
est une commande simple. Mais vous pouvez faire des choses plus sophistiquées avec eval
ou des fonctions.)
Alimente un co-traitement des données:
zpty -w x some data
Lire les données du co-traitement (dans le cas le plus simple):
zpty -r x var
Comme expect
, il peut attendre une sortie du co-processus correspondant à un modèle donné.
La syntaxe bash est beaucoup plus récente et s'appuie sur une nouvelle fonctionnalité récemment ajoutée à ksh93, bash et zsh. Il fournit une syntaxe pour permettre la gestion des descripteurs de fichiers alloués dynamiquement au-dessus de 10.
bash
offre une base coproc
syntaxe, et une extension un.
La syntaxe de base pour démarrer un co-processus ressemble à celle de zsh
:
coproc cmd
Dans ksh
ou zsh
, les tuyaux vers et depuis le co-processus sont accessibles avec >&p
Et <&p
.
Mais dans bash
, les descripteurs de fichier du canal du co-processus et de l'autre canal vers le co-processus sont retournés dans le tableau $COPROC
(Respectivement ${COPROC[0]}
Et ${COPROC[1]}
. Donc…
Alimentez les données en co-processus:
echo xxx >&"${COPROC[1]}"
Lire les données du co-processus:
read var <&"${COPROC[0]}"
Avec la syntaxe de base, vous ne pouvez démarrer qu'un seul co-processus à la fois.
Dans la syntaxe étendue, vous pouvez name vos co-processus (comme dans zsh
co-processus vides):
coproc mycoproc { cmd; }
La commande has doit être une commande composée. (Remarquez comment l'exemple ci-dessus rappelle function f { ...; }
.)
Cette fois, les descripteurs de fichiers sont dans ${mycoproc[0]}
Et ${mycoproc[1]}
.
Vous pouvez démarrer plusieurs coprocessus à la fois, mais vous faites recevez un avertissement lorsque vous démarrez un coprocessus alors qu'il est en cours d'exécution (même en mode non interactif).
Vous pouvez fermer les descripteurs de fichiers lorsque vous utilisez la syntaxe étendue.
coproc tr { tr a b; }
echo aaa >&"${tr[1]}"
exec {tr[1]}>&-
cat <&"${tr[0]}"
Notez que la fermeture de cette façon ne fonctionne pas dans les versions bash antérieures à 4.3 où vous devez l'écrire à la place:
fd=${tr[1]}
exec {fd}>&-
Comme dans ksh
et zsh
, ces descripteurs de fichier de canal sont marqués comme close-on-exec.
Mais dans bash
, la seule façon de les transmettre aux commandes exécutées est de les dupliquer sur fds 0
, 1
Ou 2
. Cela limite le nombre de co-processus avec lesquels vous pouvez interagir pour une seule commande. (Voir ci-dessous pour un exemple.)
yash
n'a pas de fonction de co-processus en soi, mais le même concept peut être implémenté avec son pipeline et processus fonctionnalités de redirection. yash
a une interface avec l'appel système pipe()
, donc ce genre de chose peut être fait assez facilement à la main.
Vous commenceriez un co-processus avec:
exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
Ce qui crée d'abord une pipe(4,5)
(5 la fin de l'écriture, 4 la fin de la lecture), puis redirige fd 3 vers un canal vers un processus qui s'exécute avec son stdin à l'autre extrémité, et stdout allant vers le canal créé plus tôt. Ensuite, nous fermons l'extrémité d'écriture de ce canal dans le parent dont nous n'aurons pas besoin. Alors maintenant, dans le Shell, nous avons fd 3 connecté à stdin du cmd et fd 4 connecté à stdout de cmd avec des tuyaux.
Notez que l'indicateur close-on-exec n'est pas défini sur ces descripteurs de fichiers.
Pour alimenter des données:
echo data >&3 4<&-
Pour lire les données:
read var <&4 3>&-
Et vous pouvez fermer les fds comme d'habitude:
exec 3>&- 4<&-
Les coprocessus peuvent facilement être mis en œuvre avec des tuyaux nommés standard. Je ne sais pas quand les tubes nommés exactement ont été introduits mais il est possible que ce soit après que ksh
soit venu avec des co-processus (probablement au milieu des années 80, ksh88 a été "publié" en 88, mais je crois que ksh
a été utilisé en interne chez AT&T quelques années auparavant), ce qui expliquerait pourquoi.
cmd |&
echo data >&p
read var <&p
Peut être écrit avec:
mkfifo in out
cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
Interagir avec ceux-ci est plus simple, surtout si vous devez exécuter plus d'un co-processus. (Voir les exemples ci-dessous.)
Le seul avantage de l'utilisation de coproc
est que vous n'avez pas à nettoyer ces canaux nommés après utilisation.
Les obus utilisent des tuyaux dans quelques constructions:
cmd1 | cmd2
,$(cmd)
,<(cmd)
, >(cmd)
.Dans ceux-ci, les données circulent dans une seule direction entre les différents processus.
Avec les co-processus et les canaux nommés, cependant, il est facile de se retrouver dans une impasse. Vous devez garder une trace de quelle commande a quel descripteur de fichier ouvert, pour éviter qu'une ne reste ouverte et ne maintienne un processus en vie. Les blocages peuvent être difficiles à étudier, car ils peuvent se produire de manière non déterministe; par exemple, uniquement lorsque autant de données que pour remplir un tuyau sont envoyées.
expect
pour ce pour quoi il a été conçuLe but principal des co-processus était de fournir au Shell un moyen d'interagir avec les commandes. Cependant, cela ne fonctionne pas aussi bien.
La forme la plus simple de blocage mentionnée ci-dessus est:
tr a b |&
echo a >&p
read var<&p
Parce que sa sortie ne va pas à un terminal, tr
met en mémoire tampon sa sortie. Donc, il ne sortira rien tant qu'il n'aura pas vu la fin du fichier sur son stdin
, ou qu'il aura accumulé un tampon plein de données à produire. Donc ci-dessus, une fois que le Shell a sorti a\n
(Seulement 2 octets), le read
se bloquera indéfiniment parce que tr
attend que le Shell lui envoie plus de données.
En bref, les tuyaux ne sont pas bons pour interagir avec les commandes. Les coprocessus ne peuvent être utilisés que pour interagir avec des commandes qui ne tamponnent pas leur sortie, ou commandes qui peuvent être invitées à ne pas mettre leur sortie en tampon; par exemple, en utilisant stdbuf
avec certaines commandes sur les systèmes récents GNU ou FreeBSD.
C'est pourquoi expect
ou zpty
utilisent plutôt des pseudo-terminaux. expect
est un outil conçu pour interagir avec les commandes, et il le fait bien.
Les coprocessus peuvent être utilisés pour réaliser des travaux de plomberie plus complexes que ne le permettent les simples tuyaux Shell.
cette autre réponse Unix.SE a un exemple d'utilisation de coproc.
Voici un exemple simplifié: Imaginez que vous voulez une fonction qui alimente une copie de la sortie d'une commande vers 3 autres commandes, puis avoir la sortie de ces 3 commandes se concaténer.
Tous utilisant des tuyaux.
Par exemple: alimentez la sortie de printf '%s\n' foo bar
Vers tr a b
, sed 's/./&&/g'
Et cut -b2-
Pour obtenir quelque chose comme:
foo
bbr
ffoooo
bbaarr
oo
ar
Tout d'abord, ce n'est pas nécessairement évident, mais il y a une possibilité de blocage, et cela commencera à se produire après seulement quelques kilo-octets de données.
Ensuite, selon votre Shell, vous rencontrerez un certain nombre de problèmes différents qui doivent être traités différemment.
Par exemple, avec zsh
, vous le feriez avec:
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
Ci-dessus, les fds de coprocessus ont l'indicateur close-on-exec défini, mais pas ceux qui en sont dupliqués (comme dans {o1}<&p
). Donc, pour éviter les blocages, vous devez vous assurer qu'ils sont fermés dans tous les processus qui n'en ont pas besoin.
De même, nous devons utiliser un sous-shell et utiliser exec cat
À la fin, pour nous assurer qu'il n'y a pas de processus Shell sur le fait de maintenir un tuyau ouvert.
Avec ksh
(ici ksh93
), Cela devrait être:
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
( Remarque: Cela ne fonctionnera pas sur les systèmes où ksh
utilise socketpairs
au lieu de pipes
, et où /dev/fd/n
fonctionne comme sous Linux.)
Dans ksh
, les fds au-dessus de 2
Sont marqués avec l'indicateur close-on-exec, sauf s'ils sont transmis explicitement sur la ligne de commande. C'est pourquoi nous n'avons pas à fermer les descripteurs de fichiers inutilisés comme avec zsh
— mais c'est aussi pourquoi nous devons faire {i1}>&$i1
Et utiliser eval
pour cette nouvelle valeur de $i1
, À transmettre à tee
et cat
…
Dans bash
cela ne peut pas être fait, car vous ne pouvez pas éviter l'indicateur close-on-exec.
Ci-dessus, c'est relativement simple, car nous utilisons uniquement des commandes externes simples. Cela devient plus compliqué lorsque vous souhaitez utiliser des constructions Shell à la place et que vous commencez à rencontrer des bogues Shell.
Comparez ce qui précède avec le même en utilisant des tuyaux nommés:
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
Si vous souhaitez interagir avec une commande, utilisez expect
ou zsh
, ou [tubes nommés] zpty
.
Si vous voulez faire de la plomberie avec des tuyaux, utilisez des tuyaux nommés.
Les coprocessus peuvent faire une partie de ce qui précède, mais soyez prêt à vous gratter sérieusement la tête pour tout ce qui n'est pas trivial.
Les co-processus ont d'abord été introduits dans un langage de script Shell avec le ksh88
Shell (1988), et plus tard dans zsh
à un moment donné avant 1993.
La syntaxe pour lancer un co-processus sous ksh est command |&
. À partir de là, vous pouvez écrire dans command
entrée standard avec print -p
et lire sa sortie standard avec read -p
.
Plus de deux décennies plus tard, bash qui manquait de cette fonctionnalité l'a finalement introduit dans sa version 4.0. Malheureusement, une syntaxe incompatible et plus complexe a été sélectionnée.
Sous bash 4.0 et plus récent, vous pouvez lancer un co-processus avec la commande coproc
, par exemple:
$ coproc awk '{print $2;fflush();}'
Vous pouvez ensuite passer quelque chose à la commande stdin de cette façon:
$ echo one two three >&${COPROC[1]}
et lire la sortie awk avec:
$ read -ru ${COPROC[0]} foo
$ echo $foo
two
Sous ksh, cela aurait été:
$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
Qu'est-ce qu'un "coproc"?
C'est l'abréviation de "co-process" qui signifie un deuxième processus coopérant avec Shell. Il est très similaire à un travail d'arrière-plan commencé par un "&" à la fin de la commande, sauf qu'au lieu de partager les mêmes entrées et sorties standard que son shell parent, ses E/S standard sont connectées au shell parent par un spécial type de pipe appelé FIFO.Pour référence cliquez ici
On démarre un coproc en zsh avec:
coproc command
La commande doit être préparée pour lire à partir de stdin et/ou écrire sur stdout, ou elle n'est pas très utile en tant que coproc.
Lire cet article ici il fournit une étude de cas entre exec et coproc
Voici un autre bon exemple (et fonctionnel) - un simple serveur écrit en BASH. Veuillez noter que vous auriez besoin du netcat
d'OpenBSD, le classique ne fonctionnera pas. Bien sûr, vous pouvez utiliser le socket inet au lieu d'unix.
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
Usage:
$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$