web-dev-qa-db-fra.com

Comment faire un pipe bidirectionnel entre deux programmes?

Tout le monde sait comment faire un pipe unidirectionnel entre deux programmes (lier stdout du premier et stdin du second): first | second.

Mais comment créer un tube bidirectionnel, c'est-à-dire réticuler stdin et stdout de deux programmes? Existe-t-il un moyen facile de le faire dans un shell?

66
user14284

Si les canaux de votre système sont bidirectionnels (comme ils le sont sur Solaris 11 et certains BSD au moins, mais pas Linux):

cmd1 <&1 | cmd2 >&0

Attention aux impasses cependant.

Notez également que certaines versions de ksh93 sur certains systèmes implémentent des canaux (|) Utilisant un socket socket. les paires de sockets sont bidirectionnelles, mais ksh93 arrête explicitement le sens inverse, de sorte que la commande ci-dessus ne fonctionnerait pas avec ces ksh93 même sur les systèmes où les canaux (tels que créés par l'appel système pipe(2)) sont bidirectionnels.

32

Eh bien, c'est assez "facile" avec des pipes nommées (mkfifo). Je mets facile entre guillemets car à moins que les programmes ne soient conçus pour cela, un blocage est probable.

mkfifo fifo0 fifo1
( prog1 > fifo0 < fifo1 ) &
( prog2 > fifo1 < fifo0 ) &
( exec 30<fifo0 31<fifo1 )      # write can't open until there is a reader
                                # and vice versa if we did it the other way

Maintenant, il y a normalement un tampon impliqué dans l'écriture de stdout. Ainsi, par exemple, si les deux programmes étaient:

#!/usr/bin/Perl
use 5.010;
say 1;
print while (<>);

vous vous attendriez à une boucle infinie. Mais au lieu de cela, les deux seraient dans l'impasse; vous devez ajouter $| = 1 (ou équivalent) pour désactiver la mise en mémoire tampon de sortie. Le blocage est dû au fait que les deux programmes attendent quelque chose sur stdin, mais ils ne le voient pas car il se trouve dans le tampon stdout de l'autre programme , et n'a pas encore été écrit sur le tuyau.

Mise à jour: intégrant les suggestions de Stéphane Charzelas et Joost:

mkfifo fifo0 fifo1
prog1 > fifo0 < fifo1 &
prog2 < fifo0 > fifo1

fait de même, est plus court et plus portable.

43
derobert

Je ne sais pas si c'est ce que vous essayez de faire:

nc -l -p 8096 -c second &
nc -c first 127.0.0.1 8096 &

Cela commence par l'ouverture d'un socket d'écoute sur le port 8096, et une fois la connexion établie, le programme second avec son stdin comme sortie de flux et stdout comme entrée de flux.

Ensuite, un deuxième nc est lancé qui se connecte au port d'écoute et génère le programme first avec son stdout comme entrée de flux et son stdin comme flux production.

Cela ne se fait pas exactement à l'aide d'un tuyau, mais il semble faire ce dont vous avez besoin.

Comme cela utilise le réseau, cela peut être fait sur 2 ordinateurs distants. C'est presque ainsi qu'un serveur Web (second) et un navigateur Web (first) fonctionnent.

13
jfg956

Vous pouvez utiliser pipexec :

$ pipexec -- '[A' cmd1 ] '[B' cmd2 ] '{A:1>B:0}' '{B:1>A:0}'
10
Andreas Florath

bash la version 4 dispose de la commande coproc qui permet de le faire en pur bash sans canaux nommés:

coproc cmd1
eval "exec cmd2 <&${COPROC[0]} >&${COPROC[1]}"

Certains autres shells peuvent également faire coproc.

La réponse ci-dessous est plus détaillée mais enchaîne trois commandes, plutôt que deux, ce qui rend la commande un peu plus intéressante.

Si vous êtes heureux d'utiliser également cat et stdbuf alors la construction peut être rendue plus facile à comprendre.

Version utilisant bash avec cat et stdbuf, facile à comprendre:

# start pipeline
coproc {
    cmd1 | cmd2 | cmd3
}
# create command to reconnect STDOUT `cmd3` to STDIN of `cmd1`
endcmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"
# eval the command.
eval "${endcmd}"

Notez que vous devez utiliser eval car l'expansion des variables dans <& $ var est illégale dans ma version de bash 4.2.25.

Version utilisant pure bash: Se décompose en deux parties, lance le premier pipeline sous coproc, puis lance la deuxième partie (soit une seule commande soit un pipeline) en le reconnectant à la première:

coproc {
    cmd 1 | cmd2
}
endcmd="exec cmd3 <&${COPROC[0]} >&${COPROC[1]}"
eval "${endcmd}"

Preuve de concept:

fichier ./prog, juste un programme factice pour consommer, étiqueter et réimprimer les lignes. L'utilisation de sous-coquilles pour éviter les problèmes de mise en mémoire tampon peut être exagérée, ce n'est pas le but ici.

#!/bin/bash
let c=0
sleep 2

[ "$1" == "1" ] && ( echo start )

while : ; do
  line=$( head -1 )
  echo "$1:${c} ${line}" 1>&2
  sleep 2
  ( echo "$1:${c} ${line}" )
  let c++
  [ $c -eq 3 ] && exit
done

fichier ./start_cat Ceci est une version utilisant bash, cat et stdbuf

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2 \
    | stdbuf -i0 -o0 ./prog 3
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

ou fichier ./start_part. Il s'agit d'une version utilisant uniquement bash pur. À des fins de démonstration, j'utilise toujours stdbuf parce que votre véritable prog devrait de toute façon gérer la mise en mémoire tampon pour éviter le blocage dû à la mise en mémoire tampon.

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 ./prog 3 <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

Production:

> ~/iolooptest$ ./start_part
starting first cmd
Delaying remainer
2:0 start
Running: exec stdbuf -i0 -o0 ./prog 3 <&63 >&60
3:0 2:0 start
1:0 3:0 2:0 start
2:1 1:0 3:0 2:0 start
3:1 2:1 1:0 3:0 2:0 start
1:1 3:1 2:1 1:0 3:0 2:0 start
2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
1:2 3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start

Ça le fait.

7
AnyDev

Un bloc de construction pratique pour écrire de tels tuyaux bidirectionnels est quelque chose qui relie ensemble la sortie standard et la sortie standard du processus en cours. Appelons-le ioloop. Après avoir appelé cette fonction, il vous suffit de démarrer un canal normal:

ioloop &&     # stdout -> stdin 
cmd1 | cmd2   # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)

Si vous ne souhaitez pas modifier les descripteurs du shell de niveau supérieur, exécutez-le dans un sous-shell:

( ioloop && cmd1 | cmd2 )

Voici une implémentation portable d'ioloop utilisant un canal nommé:

ioloop() {
    FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
    trap "rm -f $FIFO" EXIT &&
    mkfifo $FIFO &&
    ( : <$FIFO & ) &&    # avoid deadlock on opening pipe
    exec >$FIFO <$FIFO
}

Le canal nommé n'existe que brièvement dans le système de fichiers pendant la configuration d'ioloop. Cette fonction n'est pas tout à fait POSIX car mktemp est déconseillé (et potentiellement vulnérable à une attaque raciale).

Une implémentation spécifique à Linux utilisant/proc/est possible qui ne nécessite pas de canal nommé, mais je pense que celui-ci est plus que suffisant.

5
user873942

Il y a aussi

Comme @ StéphaneChazelas le note correctement dans les commentaires, les exemples ci-dessus sont le "formulaire de base", il a Bons exemples avec options sur sa réponse pour un question similaire .

5
Alex Stragies

Il y a beaucoup de bonnes réponses ici. Je veux donc simplement ajouter quelque chose pour jouer facilement avec eux. Je suppose que stderr n'est redirigé nulle part. Créez deux scripts (disons a.sh et b.sh):

#!/bin/bash
echo "foo" # change to 'bar' in second file

for i in {1..10}; do
  read input
  echo ${input}
  echo ${i} ${0} got: ${input} >&2
done

Ensuite, lorsque vous les connectez, vous devriez voir sur la console:

1 ./a.sh got: bar
1 ./b.sh got: foo
2 ./a.sh got: foo
2 ./b.sh got: bar
3 ./a.sh got: bar
3 ./b.sh got: foo
4 ./a.sh got: foo
4 ./b.sh got: bar
...
0
pawel7318