Comment faire un tuyau bidirectionnel entre deux programmes?

63

Tout le monde sait comment faire tuyau unidirectionnel entre deux programmes (bind stdoutde première et stdinde seconde): first | second.

Mais comment faire un tuyau bidirectionnel, c'est-à-dire une liaison croisée stdinet stdoutde deux programmes? Y a-t-il un moyen facile de le faire dans une coquille?


la source

Réponses:

30

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

cmd1 <&1 | cmd2 >&0

Attention cependant aux impasses.

Notez également que certaines versions de ksh93 sur certains systèmes implémentent pipes ( |) en utilisant une paire de sockets . les paires de sockets sont bidirectionnelles, mais ksh93 arrête explicitement le sens inverse, de sorte que la commande ci-dessus ne fonctionne pas avec ces ksh93, même sur les systèmes où les pipes (créées par l' pipe(2)appel système) sont bidirectionnelles.

Stéphane Chazelas
la source
1
est-ce que quelqu'un sait si cela fonctionne aussi sur Linux? (en utilisant archlinux ici)
heinrich5991
41

Eh bien, c’est assez "facile" avec des pipes nommées ( mkfifo). Je mets easy 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. Donc, 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 auriez besoin d'ajouter $| = 1(ou l'é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 ils sont dans le tampon stdout de l'autre programme et n'ont pas encore été écrits dans le tuyau.

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

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

fait la même chose, est plus court et plus portable.

derobert
la source
23
Un tube nommé suffit: prog1 < fifo | prog2 > fifo.
Andrey Vihrov
2
@AndreyVihrov c'est vrai, vous pouvez remplacer un tube anonyme par l'un des tubes nommés. Mais j'aime bien la symétrie :-P
derobert
3
@ user14284: Sous Linux, vous pouvez probablement le faire avec quelque chose comme prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifo.
Andrey Vihrov
3
Si vous le faites prog2 < fifo0 > fifo1, vous pouvez éviter votre petite danse avec exec 30< ...(qui d'ailleurs ne fonctionne qu'avec bashou yashpour des fds supérieurs à 10 comme ça).
Stéphane Chazelas
1
@ Joost Hmmm, il semble que vous ayez raison de dire qu'il n'est pas nécessaire, du moins en bash. J'étais probablement inquiet que, puisque le shell effectue les redirections (y compris en ouvrant les canaux), il risque de bloquer, mais au moins bash forks avant d'ouvrir le fifo. dashsemble bien aussi (mais se comporte un peu différemment)
derobert
13

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 ouvrir un socket d’écoute sur le port 8096 et, une fois la connexion établie, lance le programme en spécifiant secondson stdincomme sortie de flux et stdoutson entrée de flux.

Ensuite, une seconde ncest lancée qui se connecte au port d’écoute et génère le programme firstavec son stdoutentrée et stdinla sortie du flux.

Ce n'est pas exactement fait en utilisant 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 que fonctionnent un serveur Web ( second) et un navigateur Web ( first).

jfg956
la source
1
Et nc -Upour les sockets de domaine UNIX qui prennent uniquement un espace d'adressage de système de fichiers.
Ciro Santilli a annoncé
L'option -c n'est pas disponible sous Linux. Mon bonheur fut de courte durée :-(
pablochacin
9

Vous pouvez utiliser pipexec :

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

bashla version 4 a une coproccommande qui permet de le faire en pur bashsans les pipes nommées:

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

D'autres coquilles peuvent aussi faire coprocaussi bien.

Vous trouverez ci-dessous une réponse plus détaillée, mais trois chaînes au lieu de deux, ce qui en fait un peu plus intéressant.

Si vous êtes heureux d'utiliser aussi catet stdbufpuis construire peut être plus facile à comprendre.

Version utilisant bashavec catet 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 le développement de variable dans <& $ var est illégal dans ma version de bash 4.2.25.

Version using pure bash: divisez en deux parties, lancez le premier pipeline sous coproc, puis lancez la deuxième partie (une seule commande ou un pipeline) en la 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 à utiliser, baliser et réimprimer des lignes. Utiliser des sous-réservoirs pour éviter les problèmes de mise en mémoire tampon est peut-être excessif, ce n'est pas le problème.

#!/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

file ./start_cat Ceci est une version utilisant bash, catetstdbuf

#!/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. Ceci est une version utilisant pur bashuniquement. Pour les besoins de la démonstration, je l’utilise encore stdbufcar votre programme réel devra de toute façon gérer la mise en mémoire tampon en interne pour éviter le blocage en raison de 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}"

Sortie:

> ~/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.

AnyDev
la source
5

Un bloc de construction pratique pour l'écriture de tels canaux bidirectionnels est quelque chose qui relie le stdout et le stdin du processus en cours. Appelons-le ioloop. Après avoir appelé cette fonction, il vous suffit de démarrer un tuyau normal:

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

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

( ioloop && cmd1 | cmd2 )

Voici une implémentation portable de ioloop utilisant un tube 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 lors de la configuration de ioloop. Cette fonction n'est pas tout à fait POSIX car mktemp est obsolète (et potentiellement vulnérable à une attaque de race).

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

utilisateur873942
la source
Fonction intéressante, +1. Pourrait probablement utiliser une phrase ou 2 ajouté expliquant ( : <$FIFO & )plus en détail. Merci d'avoir posté.
Alex Stragies
J'ai un peu regardé autour de moi et suis sorti blanc; Où puis-je trouver des informations sur la dépréciation de mktemp? Je l'utilise beaucoup, et si un outil plus récent a pris sa place, j'aimerais commencer à l'utiliser.
DopeGhoti
Alex: L'appel ouvert (2) sur un tuyau naned est bloquant. si vous essayez de "exec <$ PIPE> $ PIPE", il sera bloqué dans l'attente d'un autre processus pour ouvrir l'autre côté. La commande ": <$ FIFO &" est exécutée dans un sous-shell en arrière-plan et permet à la redirection bidirectionnelle de se terminer correctement.
user873942
DopeGhoti: la fonction de bibliothèque mktemp (3) C est obsolète. L'utilitaire mktemp (1) ne l'est pas.
user873942
4

Il y a aussi

Comme @ StéphaneChazelas a correctement noté dans les commentaires, les exemples ci-dessus sont la "forme de base", il a de bons exemples avec des options sur sa réponse à une question similaire .

Alex Stragies
la source
Notez que par défaut, socatutilise des sockets au lieu de tubes (vous pouvez changer cela avec commtype=pipes). Vous souhaiterez peut-être ajouter cette noforkoption pour éviter qu'un processus socat supplémentaire ne transfère des données entre les canaux / sockets. (merci pour l'édition sur ma réponse d' ailleurs)
Stéphane Chazelas
0

Il y a beaucoup de bonnes réponses ici. Donc, je veux juste ajouter quelque chose pour jouer facilement avec eux. Je suppose que stderrn'est pas 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 connecterez, 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
...
pawel7318
la source