web-dev-qa-db-fra.com

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

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

84
slm

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.

co-processus ksh

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

co-processus zsh

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é.

co-processus bash

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.

Syntaxe de base

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.

Syntaxe étendue

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.)

processus yash et redirection de pipeline

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

Maintenant, pourquoi ils ne sont pas si populaires

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

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.

enclin à l'impasse

Les obus utilisent des tuyaux dans quelques constructions:

  • Tubes shell: cmd1 | cmd2,
  • substitution de commande: $(cmd),
  • et substitution de processus: <(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.

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

Le 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.

La gestion des descripteurs de fichiers est fastidieuse et difficile à obtenir correctement

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

Conclusion

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.

124
Stéphane Chazelas

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

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

0
Munai Das Udasin

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)
$
0
Alexey Naidyonov