Comment utilisez-vous la command coproc dans Bash?

Quelqu'un peut-il fournir quelques exemples sur la façon d'utiliser coproc ?

les co-process sont une fonctionnalité ksh (déjà dans ksh88 ). zsh a eu la fonctionnalité dès le début (début des années 90), alors qu'il vient d'être ajouté à bash en 4.0 (2009).

Cependant, le comportement et l'interface sont significativement différents entre les 3 obus.

L'idée est la même: elle permet de démarrer un travail en arrière-plan et de pouvoir l'envoyer en input et lire sa sortie sans devoir recourir à des pipes nommées.

Cela se fait avec des tuyaux sans nom avec la plupart des shells et des paires de socket avec des versions récentes de ksh93 sur certains systèmes.

Dans a | cmd | b a | cmd | b a | cmd | b , a alimente datatables en cmd et b lit sa sortie. Exécuter cmd tant que co-process permet à l'interpréteur de commands d'être à la fois a et b .

ksh co-process

Dans ksh , vous démarrez un coprocess comme:

 cmd |& 

Vous alimentez des données en 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 lancé comme n'importe quel travail en arrière-plan, vous pouvez utiliser fg , bg , kill dessus et le referencer par %job-number ou via $! .

Pour fermer la fin d'écriture de la cmd lecture, vous pouvez faire:

 exec 3>&p 3>&- 

Et pour fermer la fin de la lecture de l'autre pipe (la seule cmd écrit):

 exec 3<&p 3<&- 

Vous ne pouvez pas démarrer un second co-process sauf si vous enregistrez d'abord les descripteurs de files de pipe sur d'autres files. Par exemple:

 tr ab |& exec 3>&p 4<&p tr bc |& echo aaa >&3 echo bbb >&p 

zsh co-process

En zsh , les co-process sont presque identiques à ceux de ksh . La seule vraie différence est que les co-process zsh sont démarrés avec le mot key coproc .

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

Faire:

 exec 3>&p 

Note: Cela ne déplace pas le descripteur de file coproc sur fd 3 (comme dans ksh ), mais le duplique. Donc, il n'y a pas de moyen explicite de fermer l'alimentation ou de lire le tuyau, l'autre commençant un autre coproc .

Par exemple, pour fermer l'extrémité d'alimentation:

 coproc tr ab 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-process basés sur les pipes, zsh (depuis 3.1.6-dev19, publié en 2000) a des constructions pseudo-tty comme expect . Pour interagir avec la plupart des programmes, les co-process de type ksh ne fonctionneront pas, car les programmes commencent à mettre en memory tampon lorsque leur sortie est un canal.

Voici quelques exemples.

Lancer le co-process x :

 zmodload zsh/zpty zpty x cmd 

(Ici, cmd est une simple command, mais vous pouvez faire des choses plus intéressantes avec eval ou des fonctions.)

Fournir des données de co-process:

 zpty -wx some data 

Lire des données de traitement conjoint (dans le cas le plus simple):

 zpty -rx var 

Comme expect , il peut attendre une sortie du process de co-traitement correspondant à un model donné.

bash co-process

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 permettant la gestion des descripteurs de files alloués dynamicment au-dessus de 10.

bash offre une syntaxe coproc basique et une syntaxe étendue .

Syntaxe de base

La syntaxe de base pour démarrer un co-process ressemble à celle de zsh :

 coproc cmd 

Dans ksh ou zsh , les canaux vers et depuis le co-process sont accessibles avec >&p et <&p .

Mais dans bash , les descripteurs de file du pipe du co-process et de l'autre pipe au co-process sont returnnés dans le tableau $COPROC (respectivement ${COPROC[0]} et ${COPROC[1]} . …

Flux de données vers le co-process:

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

Lire datatables du co-process:

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

Avec la syntaxe de base, vous ne pouvez démarrer qu'un seul co-process à la fois.

Syntaxe étendue

Dans la syntaxe étendue, vous pouvez nommer vos co-process (comme dans les co-process zsh zpty):

 coproc mycoproc { cmd; } 

La command doit être une command composée. (Remarquez comment l'exemple ci-dessus callbackle la function f { ...; } .)

Cette fois, les descripteurs de files sont dans ${mycoproc[0]} et ${mycoproc[1]} .

Vous pouvez démarrer plus d'un co-process à la fois, mais vous obtenez un avertissement lorsque vous démarrez un co-process pendant que vous êtes encore en cours d'exécution (même en mode non interactif).

Vous pouvez fermer les descripteurs de files lorsque vous utilisez la syntaxe étendue.

 coproc tr { tr ab; } 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 files de pipe sont marqués comme close-on-exec.

Mais dans bash , la seule façon de les transmettre aux commands exécutées est de les dupliquer sur fds 0 , 1 ou 2 . Cela limite le nombre de co-process avec lesquels vous pouvez interagir pour une seule command. (Voir ci-dessous pour un exemple.)

process yash et redirection de pipeline

yash n'a pas de fonctionnalité de co-traitement en soi, mais le même concept peut être implémenté avec ses fonctionnalités de redirection de pipeline et de process . yash a une interface avec l'appel système pipe() , donc ce genre de chose peut être fait relativement facilement à la main.

Vous commenceriez un co-process avec:

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

Le premier crée un pipe(4,5) (5 l'extrémité d'écriture, 4 la fin de lecture), redirige ensuite fd 3 vers un canal qui exécute son stdin à l'autre extrémité et stdout vers le canal créé précédemment . Ensuite, nous fermons la fin d'écriture de ce tuyau chez le parent dont nous n'aurons pas besoin. Alors maintenant dans la coquille nous avons fd 3 connecté à la std de la 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 file.

Pour alimenter datatables:

 echo data >&3 4<&- 

Pour lire des données:

 read var <&4 3>&- 

Et vous pouvez fermer fds comme d'habitude:

 exec 3>&- 4<&- 

Maintenant, pourquoi ils ne sont pas si populaires

presque aucun avantage sur l'utilisation de tuyaux nommés

Les co-process peuvent facilement être implémentés avec des tubes nommés standard. Je ne sais pas quand des tubes nommés ont été introduits, mais il est possible que ce soit après que ksh eu des co-process (probablement au milieu des années 80, ksh88 était "sorti" en 88, mais je crois que ksh était utilisé en interne chez AT & T années avant cela) 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 avez besoin d'exécuter plus d'un co-process. (Voir les exemples ci-dessous.)

Le seul avantage de l'utilisation de coproc est que vous n'avez pas à nettoyer ces tuyaux nommés après utilisation.

impasse

Les réservoirs utilisent des tuyaux dans quelques constructions:

  • pipes de coquille: cmd1 | cmd2 cmd1 | cmd2 ,
  • substitution de command: $(cmd) ,
  • et la substitution de process: <(cmd) , >(cmd) .

Dans ceux-ci, datatables ne circulent que dans un seul sens entre différents process.

Avec les co-process et les pipes nommés, cependant, il est facile de se refind dans l'impasse. Vous devez savoir quelle command possède quel descripteur de file ouvert, pour empêcher que l'user rest ouvert et maintienne un process en vie. Les impasses peuvent être difficiles à étudier, car elles peuvent se produire de manière non déterministe; par exemple, uniquement lorsque l'on envoie autant de données pour remplir une canalisation.

fonctionne pire que expect pour ce qu'il a été conçu pour

Le but principal des co-process était de fournir au shell un moyen d'interagir avec les commands. Cependant, cela ne fonctionne pas très bien.

La forme la plus simple d'impasse mentionnée ci-dessus est la suivante:

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

Parce que sa sortie ne va pas à un terminal, tr tamponne sa sortie. Donc, il ne sortira rien tant qu'il ne verra pas de fin de file sur son stdin , ou qu'il a accumulé un buffer-full de données à sortir. Ainsi ci-dessus, après que le shell a généré a\n (seulement 2 octets), la read se bloquera indéfiniment car tr attend que le shell lui envoie plus de données.

En bref, les pipes ne sont pas bonnes pour interagir avec les commands. Les co-process ne peuvent être utilisés que pour interagir avec des commands qui ne tamponnent pas leur sortie, ou des commands qui peuvent être dites de ne pas tamponner leur sortie; par exemple, en utilisant stdbuf avec certaines commands sur les systèmes GNU ou FreeBSD récents.

C'est pourquoi expect ou zpty utilisent à la place des pseudo-terminaux. expect est un outil conçu pour interagir avec les commands, et il le fait bien.

La gestion des descripteurs de files est fastidieuse et difficile à get

Les co-process peuvent être utilisés pour effectuer des travaux de plomberie plus complexes que ce que permettent les simples tuyaux en coquille.

cette autre réponse Unix.SE a un exemple d'utilisation coproc.

Voici un exemple simplifié: Imaginez que vous voulez une fonction qui transmet une copy de la sortie d'une command à 3 autres commands, puis que la sortie de ces trois commands soit concaténée.

Tout en utilisant des tuyaux.

Par exemple: alimente la sortie de printf '%s\n' foo bar en tr ab , sed 's/./&&/g' et cut -b2- pour get 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 après seulement quelques kilo-octets de données.

Ensuite, en fonction de votre shell, vous allez rencontrer 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 ab 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 

Au-dessus, les co-process fds ont le drapeau close-on-exec, mais pas ceux qui en sont dupliqués (comme dans {o1}<&p ). Donc, pour éviter les blocages, vous devrez vous assurer qu'ils sont fermés dans tous les process qui n'en ont pas besoin.

De la même façon, nous devons utiliser un sous-shell et utiliser un exec cat à la fin, afin de s'assurer qu'il n'y a pas de process shell qui consiste à maintenir un tuyau ouvert.

Avec ksh (ici ksh93 ), cela devrait être:

 f() ( tr ab |& 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 

( Note: cela ne fonctionnera pas sur les systèmes où ksh utilise des socketpairs au lieu de pipes et où /dev/fd/n fonctionne comme sur Linux).

Dans ksh , les fds supérieurs à 2 sont marqués avec l'indicateur close-on-exec, sauf s'ils sont transmis explicitement sur la command line. C'est pourquoi nous n'avons pas besoin de fermer les descripteurs de files inutilisés comme avec zsh mais c'est aussi pour cela que nous devons faire {i1}>&$i1 et utiliser eval pour cette nouvelle valeur de $i1 , …

En bash cela ne peut pas être fait, parce que vous ne pouvez pas éviter le drapeau close-on-exec.

Au-dessus, c'est relativement simple, parce que nous n'utilisons que des commands externes simples. Cela devient plus compliqué lorsque vous voulez utiliser des constructions shell ici, et vous commencez à courir dans 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 ab < pi1 > po1 & sed 's/./&&/g' < pi2 > po2 & cut -c2- < pi3 > po3 & tee pi{1,2} > pi3 & cat po{1,2,3} rm -fp{i,o}{1,2,3} } printf '%s\n' foo bar | f 

Conclusion

Si vous souhaitez interagir avec une command, utilisez expect , zsh ou zsh , ou les canaux nommés.

Si vous voulez faire de la plomberie de fantaisie avec des tuyaux, utilisez des tuyaux nommés.

Co-process peuvent faire certains de ce qui précède, mais être prêt à faire une tête sérieuse gratter pour quelque chose de non négligeable.

Les co-process ont été introduits dans un langage de script shell en 1988 avec ksh.

La syntaxe pour lancer un co-process sous ksh est la command |& . A partir de là, vous pouvez écrire pour command l'input standard avec print -p et lire sa sortie standard avec read -p .

Plus de quelques 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-process avec la command coproc , par exemple:

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

Vous pouvez alors passer quelque chose à la command stdin de cette façon:

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

et lisez 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 court pour "co-process" qui signifie un deuxième process de coopération avec le shell. Il est très similaire à un travail d'arrière-plan démarré avec un "&" à la fin de la command, sauf qu'au lieu de partager la même input et sortie standard que son shell parent, son E / S standard est connecté au shell parent par un sorte de tuyau appelé un FIFO.Pour reference cliquez ici

On commence un coproc dans zsh avec

 coproc command 

La command doit être préparée pour lire à partir de stdin et / ou écrire dans stdout, ou elle n'est pas très utile en tant que coproc.

Lisez cet article ici il fournit une étude de cas entre exec et coproc

Voici un autre bon exemple (et fonctionnel): un simple server écrit en BASH. Veuillez noter que vous auriez besoin du netcat d'OpenBSD, le classique ne fonctionnera pas. Bien sûr, vous pourriez utiliser 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) $