¿Cómo hacer una tubería bidireccional entre dos programas?

63

Todo el mundo sabe cómo hacer tubo unidireccional entre dos programas (bind stdoutde primera y stdinde segunda): first | second.

Pero, ¿cómo hacer una tubería bidireccional, es decir, un enlace cruzado stdiny stdoutdos programas? ¿Hay una manera fácil de hacerlo en un shell?


fuente

Respuestas:

30

Si las tuberías en su sistema son bidireccionales (como lo son en Solaris 11 y algunos BSD al menos, pero no Linux):

cmd1 <&1 | cmd2 >&0

Pero cuidado con los callejones sin salida.

También tenga en cuenta que algunas versiones de ksh93 en algunos sistemas implementan tuberías ( |) utilizando un par de sockets . los pares de zócalos son bidireccionales, pero ksh93 cierra explícitamente la dirección inversa, por lo que el comando anterior no funcionaría con esos ksh93s incluso en sistemas donde las tuberías (como las crea la pipe(2)llamada al sistema) son bidireccionales.

Stéphane Chazelas
fuente
1
¿Alguien sabe si esto también funciona en Linux? (usando archlinux aquí)
heinrich5991
41

Bueno, es bastante "fácil" con tuberías con nombre ( mkfifo). Pongo comillas fáciles porque a menos que los programas estén diseñados para esto, es probable un punto muerto.

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

Ahora, normalmente hay buffering involucrado en escribir stdout. Entonces, por ejemplo, si ambos programas fueran:

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

esperarías un bucle infinito. Pero en cambio, ambos se estancarían; necesitaría agregar $| = 1(o equivalente) para desactivar el almacenamiento en búfer de salida. El punto muerto se debe a que ambos programas están esperando algo en stdin, pero no lo ven porque está en el búfer stdout del otro programa y aún no se ha escrito en la tubería.

Actualización : incorporando sugerencias de Stéphane Charzelas y Joost:

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

hace lo mismo, es más corto y más portátil.

derobert
fuente
23
Una tubería con nombre es suficiente: prog1 < fifo | prog2 > fifo.
Andrey Vihrov
2
@AndreyVihrov es cierto, puede sustituir una tubería anónima por una de las nombradas. Pero me gusta la simetría
:-P
3
@ user14284: En Linux, probablemente pueda hacerlo con algo como prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifo.
Andrey Vihrov
3
Si lo logras prog2 < fifo0 > fifo1, puedes evitar tu pequeño baile con exec 30< ...(que por cierto solo funciona con basho yashpara mayores de 10 años así).
Stéphane Chazelas
1
@Joost Hmmm, parece que tienes razón en que no hay necesidad, al menos en bash. Probablemente estaba preocupado de que, dado que el shell realiza los redireccionamientos (incluida la apertura de las tuberías), podría bloquearse, pero al menos golpear las horquillas antes de abrir los fifos. dashparece estar bien también (pero se comporta un poco diferente)
derobert
13

No estoy seguro de si esto es lo que estás tratando de hacer:

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

Esto comienza con la apertura de un socket de escucha en el puerto 8096, y una vez que se establece una conexión, genera el programa secondcon su stdinmedida que la corriente de salida y stdoutcomo entrada de corriente.

Luego, ncse inicia un segundo que se conecta al puerto de escucha y genera el programa firstcon su stdoutentrada de flujo y su stdinsalida de flujo.

Esto no se hace exactamente usando una tubería, pero parece hacer lo que necesita.

Como esto usa la red, esto se puede hacer en 2 computadoras remotas. Esta es casi la forma en que funcionan un servidor web ( second) y un navegador web ( first).

jfg956
fuente
1
Y nc -Upara los sockets de dominio UNIX que solo ocupan espacio de direcciones del sistema de archivos.
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
La opción -c no está disponible en Linux. Mi felicidad fue de corta duración :-(
pablochacin
9

Puedes usar pipexec :

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

bashla versión 4 tiene un coproccomando que permite que esto se haga en puro bashsin canalizaciones con nombre:

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

Algunas otras conchas también pueden funcionar coproctambién.

A continuación se muestra una respuesta más detallada, pero encadena tres comandos, en lugar de dos, lo que hace que sea un poco más interesante.

Si está contento de usar también caty stdbufluego construir, puede ser más fácil de entender.

Versión bashcon caty stdbuffácil de entender:

# 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}"

Tenga en cuenta que debe usar eval porque la expansión variable en <& $ var es ilegal en mi versión de bash 4.2.25.

Versión usando pure bash: divídase en dos partes, inicie la primera tubería bajo coproc, luego almuerce la segunda parte, (ya sea un solo comando o una tubería) volviendo a conectarla a la primera:

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

Prueba de concepto:

archivo ./prog, solo un programa ficticio para consumir, etiquetar y volver a imprimir líneas. Usar subconjuntos para evitar problemas de almacenamiento en búfer puede ser excesivo, no es el punto aquí.

#!/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 Esta es una versión que usa bash, catystdbuf

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

o archivo ./start_part. Esta es una versión que usa puro bashsolamente. Para fines de demostración, todavía estoy usando stdbufporque su programa real tendría que lidiar con el almacenamiento interno de todos modos para evitar el bloqueo debido al almacenamiento en búfer.

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

Salida:

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

Eso lo hace.

AnyDev
fuente
5

Un bloque de construcción conveniente para escribir tales tuberías bidireccionales es algo que conecta el stdout y el stdin del proceso actual. Llamémoslo ioloop. Después de llamar a esta función, solo necesita iniciar una tubería normal:

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

Si no desea modificar los descriptores del shell de nivel superior, ejecute esto en un subshell:

( ioloop && cmd1 | cmd2 )

Aquí hay una implementación portátil de ioloop usando una tubería con nombre:

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

La canalización con nombre existe en el sistema de archivos solo brevemente durante la configuración de ioloop. Esta función no es POSIX porque mktemp está en desuso (y es potencialmente vulnerable a un ataque racial).

Es posible una implementación específica de Linux usando / proc / que no requiera una tubería con nombre, pero creo que esta es más que suficiente.

usuario873942
fuente
Función interesante, +1. Probablemente podría usar una oración o 2 explicaciones agregadas ( : <$FIFO & )con más detalle. Gracias por tu publicación.
Alex Stragies el
Miré un poco a mi alrededor y me quedé en blanco; ¿Dónde puedo encontrar información sobre la degradación de mktemp? Lo uso ampliamente, y si una herramienta más nueva ha ocupado su lugar, me gustaría comenzar a usarla.
DopeGhoti
Alex: el syscall abierto (2) en una tubería con revestimiento está bloqueando. si intenta "exec <$ PIPE> $ PIPE" se quedará atascado esperando que otro proceso abra el otro lado. El comando ": <$ FIFO &" se ejecuta en una subshell en segundo plano y permite que la redirección bidireccional se complete con éxito.
user873942
DopeGhoti: la función de biblioteca mktemp (3) C está en desuso. La utilidad mktemp (1) no lo es.
user873942
4

También hay

Como @ StéphaneChazelas señala correctamente en los comentarios, los ejemplos anteriores son la "forma base", tiene buenos ejemplos con opciones en su respuesta para una pregunta similar .

Alex Stragies
fuente
Tenga en cuenta que, de manera predeterminada, socatusa enchufes en lugar de tuberías (puede cambiar eso con commtype=pipes). Es posible que desee agregar la noforkopción para evitar un proceso adicional de socat que inserte datos entre las tuberías / tomas. (gracias por la edición en mi respuesta por cierto)
Stéphane Chazelas
0

Hay muchas respuestas geniales aquí. Así que solo quiero agregar algo para jugar fácilmente con ellos. Supongo stderrque no se redirige a ninguna parte. Cree dos scripts (digamos a.sh y 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

Luego, cuando los conecte de cualquier manera que debería ver en la consola:

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
fuente