Comment utilisez-vous la commande coproc dans différents shells?

Réponses:

118

les co-processus sont une kshfonctionnalité (déjà dans ksh88). zsha eu la fonction de départ (début des années 90), alors qu'il vient seulement été ajouté à bashen 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: cela permet de démarrer une tâche en arrière-plan et de pouvoir l'envoyer et la lire sans avoir à recourir à des canaux nommés.

Cela est fait avec des pipes non nommées avec la plupart des shells et socketpairs avec les versions récentes de ksh93 sur certains systèmes.

Dans a | cmd | b, aalimente les données cmdet blit sa sortie. Exécuter en cmdtant que co-processus permet au shell d'être à la fois aet b.

ksh co-processus

Dans ksh, vous démarrez un coprocessus en tant que:

cmd |&

Vous alimentez des données cmden faisant des choses comme:

echo test >&p

ou

print -p test

Et lisez cmdla sortie avec des choses comme:

read var <&p

ou

read -p var

cmdest commencé comme tout travail d'arrière - plan, vous pouvez utiliser fg, bg, killsur elle et de le renvoyer par %job-numberou via $!.

Pour fermer l’écriture du bout du tuyau cmd, vous pouvez faire:

exec 3>&p 3>&-

Et pour fermer le bout de lecture de l’autre tuyau (celui qui cmdécrit):

exec 3<&p 3<&-

Vous ne pouvez pas démarrer un deuxième co-processus à moins d’avoir préalablement enregistré les descripteurs de fichier de canal dans un autre fichier fds. Par exemple:

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

zsh co-processus

Dans zsh, les co-processus sont presque identiques à ceux de ksh. La seule différence est que les zshco-processus sont démarrés avec le coprocmot clé.

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 coprocdescripteur de fichier vers fd 3(comme dans ksh), mais le duplique. Il n’existe donc aucun moyen explicite de fermer le tuyau d’alimentation ou de lecture, un autre en commençant un autre coproc .

Par exemple, pour fermer la fin de l'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 tubes, zsh(depuis 3.1.6-dev19, publié en 2000), des constructions à base de pseudo-tty similaires expect. Pour interagir avec la plupart des programmes, les co-processus de style ksh ne fonctionneront pas, car les programmes démarrent la mise 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

(Voici cmdune commande simple. Mais vous pouvez faire des choses plus sophistiquées avec evalou des fonctions.)

Introduire des données de co-processus:

zpty -w x some data

Lire les données de co-traitement (dans le cas le plus simple):

zpty -r x var

De même expect, il peut attendre que le co-processus produise une sortie correspondant à un modèle donné.

co-processus bash

La syntaxe bash est beaucoup plus récente et repose sur une nouvelle fonctionnalité récemment ajoutée à ksh93, bash et zsh. Il fournit une syntaxe permettant le traitement des descripteurs de fichier alloués de manière dynamique au-dessus de 10.

bashpropose une syntaxe de base coproc et une syntaxe étendue .

Syntaxe de base

La syntaxe de base pour démarrer un co-processus ressemble à zshceci:

coproc cmd

Dans kshou zsh, les tuyaux entrant et sortant du co-processus sont accessibles avec >&pet <&p.

Mais dans bash, les descripteurs de fichier du canal provenant du co-processus et de l'autre canal vers le co-processus sont renvoyés dans le $COPROCtableau (respectivement ${COPROC[0]}et ${COPROC[1]}. Alors…

Transmettre les données au 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.

Syntaxe étendue

Dans la syntaxe étendue, vous pouvez nommer vos co-processus (comme dans les co-processus zshzpty):

coproc mycoproc { cmd; }

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

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

Vous pouvez commencer à plus d'un co-processus à la fois, mais vous ne recevez un avertissement lorsque vous démarrez un co-processus tandis que l' un est toujours en cours d' exécution (même en mode non-interactif).

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

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

exec {tr[1]}>&-

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

Notez que fermer de cette façon ne fonctionne pas dans les versions de bash antérieures à la 4.3, où vous devez l'écrire à la place:

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

Comme dans kshet zsh, ces descripteurs de fichier de canal sont marqués comme étant proches de l'exéc.

Mais dans bash, la seule façon de passer les commandes exécutées à est de les dupliquer à fds 0, 1ou 2. Cela limite le nombre de co-processus avec lesquels vous pouvez interagir pour une seule commande. (Voir ci-dessous pour un exemple.)

yash processus et redirection du pipeline

yashn'a pas de fonctionnalité de co-processus en soi, mais le même concept peut être mis en œuvre avec ses fonctionnalités de pipeline et de redirection de processus . yasha une interface avec l' pipe()appel système, de sorte que ce genre de chose peut être fait relativement facilement à la main.

Vous commencerez un co-processus avec:

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

Ce qui crée d'abord un pipe(4,5)(5 l'extrémité d'écriture, 4 l'extrémité de lecture), puis redirige fd 3 vers un canal vers un processus qui s'exécute avec stdin à l'autre extrémité et stdout allant au canal créé précédemment. Ensuite, nous fermons la fin d'écriture de ce tuyau dans le parent, ce dont nous n'avons pas besoin. Alors maintenant, dans le shell, nous avons fd 3 connecté au stdin de la cmd et fd 4 connecté au stdout de la cmd avec des pipes.

Notez que l'indicateur Close-on-Exec n'est pas défini sur ces descripteurs de fichier.

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<&-

Maintenant, pourquoi ils ne sont pas si populaires

pratiquement aucun avantage par rapport à l'utilisation de pipes nommées

Les co-processus peuvent facilement être mis en œuvre avec des tubes nommés standard. Je ne sais pas quand les pipes nommées exactement ont été introduites, mais il est possible que ce soit après kshdes co-processus (probablement au milieu des années 80, ksh88 a été "publié" en 88, mais je crois qu'il kshétait utilisé en interne chez AT & T quelques années auparavant. 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 ces éléments est plus simple, en particulier si vous devez exécuter plusieurs processus en même temps. (Voir exemples ci-dessous.)

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

enclin aux impasses

Les coquilles utilisent des tuyaux dans quelques constructions:

  • tuyaux shell: cmd1 | cmd2 ,
  • substitution de commande: $(cmd) ,
  • et substitution de processus: <(cmd) , >(cmd).

Dans ces derniers, les données ne circulent que dans un sens entre 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 trace de quelle commande a quel descripteur de fichier est ouvert, pour empêcher quiconque de rester ouvert et de maintenir un processus en vie. Les impasses peuvent être difficiles à enquêter, car elles peuvent survenir de manière non déterministe; par exemple, seulement quand autant de données que pour remplir un tube sont envoyées.

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

Le but principal des co-processus était de fournir au shell un moyen d'interagir avec les commandes. Cependant, cela ne fonctionne pas si bien.

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

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

Parce que sa sortie ne va pas à un terminal, elle trtamponne sa sortie. Donc, il ne restituera rien tant qu'il ne verra pas la fin du fichier stdin, ou qu'il aura accumulé toute une mémoire tampon de données à produire. Donc, ci-dessus, une fois que le shell a sorti a\n(seulement 2 octets), le readbloc sera bloqué indéfiniment car il trattend que le shell lui envoie plus de données.

En bref, les pipes ne sont pas bonnes pour interagir avec les commandes. Les co-processus ne peuvent être utilisés que pour interagir avec des commandes qui ne tamponnent pas leur sortie, ou des commandes auxquelles on peut dire de ne pas tamponner leur sortie; par exemple, en utilisant stdbufcertaines commandes sur des systèmes GNU ou FreeBSD récents.

C'est pourquoi expectou zptyutilisez plutôt des pseudo-terminaux. expectest un outil conçu pour interagir avec les commandes, et il le fait bien.

La gestion des descripteurs de fichiers est délicate et difficile à comprendre

Les co-processus peuvent être utilisés pour effectuer des travaux de plomberie plus complexes que ne le permettent les tuyaux tubulaires simples.

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

Voici un exemple simplifié: Imaginez que vous souhaitiez une fonction qui alimente une copie de la sortie d’une commande vers 3 autres commandes, puis que la sortie de ces 3 commandes soit concaténée.

Tous utilisent des tuyaux.

Par exemple: nourrir la sortie printf '%s\n' foo barà tr a b, sed 's/./&&/g'et cut -b2-d'obtenir quelque chose comme:

foo
bbr
ffoooo
bbaarr
oo
ar

Tout d’abord, ce n’est pas forcément évident, mais il existe une possibilité d’impasse, et cela commencera à se produire après seulement quelques kilo-octets de données.

Ensuite, en fonction de votre shell, vous rencontrerez un certain nombre de problèmes 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 fichiers fd du co-processus ont l'indicateur Close-on-Exec défini, mais pas ceux dupliqués à partir d'eux (comme dans {o1}<&p). Donc, pour éviter les blocages, vous devrez vous assurer qu'ils sont fermés dans tous les processus qui n'en ont pas besoin.

De la même manière, nous devons utiliser un sous-shell et l'utiliser exec catà la fin, pour nous assurer qu'il n'y a pas de processus de coque qui ne tient pas avant 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

( Note: Cela ne fonctionnera pas sur les systèmes où kshutilise au socketpairslieu de pipes, et où /dev/fd/nfonctionne comme sous Linux.)

Dans ksh, les fichiers fds ci 2- dessus sont marqués de l'indicateur close-on-exec, à moins qu'ils ne soient explicitement passés sur la ligne de commande. C'est pourquoi nous n'avons pas besoin de fermer les descripteurs de fichier inutilisés comme avec zsh— mais c'est aussi pourquoi nous devons faire {i1}>&$i1et utiliser evalpour cette nouvelle valeur de $i1, être passé à teeet cat

Dans bashce ne peut pas être fait, parce que vous ne pouvez pas éviter la fermeture sur-exec.

Ci-dessus, c'est relativement simple, car nous n'utilisons que de simples commandes externes. Cela devient plus compliqué lorsque vous souhaitez utiliser des constructions shell à la place, et que vous commencez à rencontrer des bugs shell.

Comparez ce qui précède avec le même en utilisant des tubes 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

Conclusion

Si vous souhaitez interagir avec une commande, l' utilisation expect, ou zsh« s zpty, ou les pipes nommés.

Si vous souhaitez effectuer des travaux de plomberie sophistiqués avec des tuyaux, utilisez des tuyaux nommés.

Les co-processus peuvent faire partie de ce qui est mentionné ci-dessus, mais soyez prêt à faire de sérieuses grattages pour tout ce qui n'est pas anodin.

Stéphane Chazelas
la source
Grande réponse en effet. Je ne sais pas quand cela a été corrigé, mais au moins bash 4.3.11, vous pouvez maintenant fermer les descripteurs de fichier coproc directement, sans avoir besoin d'un aux. variable; en termes de l'exemple dans votre réponse exec {tr[1]}<&- fonctionnerait maintenant (pour fermer le stdin du coproc; notez que votre code (indirectement) tente de fermer en {tr[1]}utilisant >&-, mais {tr[1]}est le stdin du coproc , et doit être fermé avec <&-). Le correctif doit être arrivé quelque part entre 4.2.25ce qui pose toujours le problème et 4.3.11ce qui ne l’est pas.
mklement0
1
@ mklement0, merci. exec {tr[1]}>&-semble en effet fonctionner avec les versions les plus récentes et est référencé dans une entrée CWRU / changelog ( permet à des mots comme {array [ind]} d'être une redirection valide ... 2012-09-01). exec {tr[1]}<&-(ou l' >&-équivalent plus correct bien que cela ne fasse aucune différence, car cela appelle simplement les close()deux) ne ferme pas le stdin du coproc, mais la fin de l'écriture du tube vers ce coproc.
Stéphane Chazelas
1
@ mklement0, bon point, je l'ai mis à jour et ajouté yash.
Stéphane Chazelas
1
Un autre avantage mkfifoest que vous n'avez pas à vous soucier des conditions de concurrence et de la sécurité pour l'accès par canalisation. Vous devez toujours vous inquiéter de l'impasse avec la fifo.
Otheus
1
À propos des blocages: la stdbufcommande peut aider à empêcher au moins certains d’entre eux. Je l'ai utilisé sous Linux et bash. Quoi qu'il en soit, @ StéphaneChazelas a raison dans la conclusion: la phase de "grattage en profondeur" ne s'est terminée que lorsque je suis revenu à des pipes nommées.
Shub
7

Les co-processus ont été introduits pour la première fois dans un langage de script shell avec le ksh88shell (1988), puis plus tard, zshavant 1993.

La syntaxe pour lancer un co-processus sous ksh est command |&. À partir de là, vous pouvez écrire sur commandl'entrée standard print -pet lire sa sortie standard avec read -p.

Plus de deux décennies plus tard, bash, qui manquait de cette fonctionnalité, l'a finalement introduite dans sa version 4.0. Malheureusement, une syntaxe incompatible et plus complexe a été sélectionnée.

Sous bash 4.0 et versions ultérieures, vous pouvez lancer un co-processus avec la coproccommande, par exemple:

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

Vous pouvez alors passer quelque chose à la commande 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
jlliagre
la source
-1

Qu'est-ce qu'un "coproc"?

En abrégé, "co-process" signifie un second processus coopérant avec le shell. Cela ressemble beaucoup à un travail en arrière-plan démarré avec 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 une connexion spéciale. type de tuyau appelé FIFO.Pour la référence cliquez ici

On commence un coproc dans zsh avec

coproc command

La commande doit être prête à lire à partir de stdin et / ou à écrire sur stdout, sinon 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

Munai Das Udasin
la source
Pouvez-vous ajouter une partie de l'article à votre réponse? J'essayais de faire en sorte que ce sujet soit traité dans U & L car il semblait sous-représenté. Merci pour votre réponse! Notez également que je mets le tag comme Bash, pas zsh.
slm
@slm Vous avez déjà indiqué les hackers Bash. J'y ai vu suffisamment d'exemples. Si votre intention était de porter cette question sous attention, alors oui, vous avez réussi:>
Valentin Bajrami
Ce ne sont pas des types de tuyaux spéciaux, ce sont les mêmes tuyaux que ceux utilisés avec |. (c’est-à-dire utilisez des tuyaux dans la plupart des shells et des socketpairs dans ksh93). les tubes et les paires de connecteurs sont les premiers entrants, les premiers sortants, ils sont tous FIFO. mkfifofait des pipes nommés, les coprocesses n'utilisent pas de pipes nommés.
Stéphane Chazelas
@slm désolé pour zsh ... en fait, je travaille sur zsh. J'ai tendance à le faire parfois avec le flux. Ça marche très bien aussi chez Bash ...
Munai Das Udasin
@ Stéphane Chazelas Je suis assez sûr que je l' ai lu quelque part que c'est d' E / S est connecté à des types de tuyaux spéciaux appelés FIFO ...
Munai Das Udasin
-1

Voici un autre bon exemple (qui fonctionne): un simple serveur écrit en BASH. Veuillez noter que vous auriez besoin d'OpenBSD netcat, le classique ne fonctionnera pas. Bien sûr, vous pouvez utiliser inet socket 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)
$
Alexey Naidyonov
la source