Puede usar una combinación de GNU stdbuf y peede moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
orinar popen(3)esas 3 líneas de comando de shell y luego freadla entrada y fwritelas tres, que se almacenarán en un búfer de hasta 1M.
La idea es tener un buffer al menos tan grande como la entrada. De esta manera, aunque los tres comandos se inicien al mismo tiempo, solo verán la entrada entrando cuando pee pcloselos tres comandos se encuentren secuencialmente.
Sobre cada uno pclose, peevacía el búfer al comando y espera su finalización. Eso garantiza que mientras esos cmdxcomandos no comiencen a generar nada antes de que hayan recibido ninguna entrada (y no bifurquen un proceso que pueda continuar emitiendo después de que su padre haya regresado), la salida de los tres comandos no será intercalado
En efecto, es un poco como usar un archivo temporal en la memoria, con el inconveniente de que los 3 comandos se inician simultáneamente.
Para evitar iniciar los comandos al mismo tiempo, puede escribir peecomo una función de shell:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Pero tenga en cuenta que los shells que zshno sean fallarían para la entrada binaria con caracteres NUL.
Eso evita el uso de archivos temporales, pero eso significa que toda la entrada se almacena en la memoria.
En cualquier caso, tendrá que almacenar la entrada en algún lugar, en la memoria o en un archivo temporal.
En realidad, es una pregunta bastante interesante, ya que nos muestra el límite de la idea de Unix de que varias herramientas simples cooperen en una sola tarea.
Aquí, nos gustaría que varias herramientas cooperen con la tarea:
- un comando fuente (aquí
echo)
- un comando de despachador (
tee)
- algunos comandos de filtro (
cmd1, cmd2, cmd3)
- y un comando de agregación (
cat).
Sería bueno si todos pudieran correr juntos al mismo tiempo y hacer su trabajo duro en los datos que deben procesar tan pronto como estén disponibles.
En el caso de un comando de filtro, es fácil:
src | tee | cmd1 | cat
Todos los comandos se ejecutan simultáneamente, cmd1comienza a masticar datos srctan pronto como está disponible.
Ahora, con tres comandos de filtro, aún podemos hacer lo mismo: iniciarlos simultáneamente y conectarlos con tuberías:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Lo que podemos hacer con relativa facilidad con tuberías con nombre :
pee() (
mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
{ tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
eval "$1 < tee-cmd1 1<> cmd1-cat &"
eval "$2 < tee-cmd2 1<> cmd2-cat &"
eval "$3 < tee-cmd3 1<> cmd3-cat &"
exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
(lo anterior } 3<&0es evitar el hecho de que &redirige stdindesde /dev/null, y usamos <>para evitar la apertura de las tuberías para bloquear hasta que el otro extremo ( cat) también se haya abierto)
O para evitar tuberías con nombre, un poco más doloroso con zshcoproc:
pee() (
n=0 ci= co= is=() os=()
for cmd do
eval "coproc $cmd $ci $co"
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
Ahora, la pregunta es: una vez que todos los programas se inicien y se conecten, ¿fluirán los datos?
Tenemos dos restricciones:
tee alimenta todas sus salidas a la misma velocidad, por lo que solo puede enviar datos a la velocidad de su tubería de salida más lenta.
cat solo comenzará a leer desde la segunda tubería (tubería 6 en el dibujo anterior) cuando todos los datos hayan sido leídos desde la primera (5).
Lo que eso significa es que los datos no fluirán en la tubería 6 hasta que cmd1haya terminado. Y, como en el caso de lo tr b Banterior, eso puede significar que los datos tampoco fluirán en la tubería 3, lo que significa que no fluirán en ninguna de las tuberías 2, 3 o 4 ya quetee alimentan a la velocidad más lenta de las 3.
En la práctica, esas tuberías tienen un tamaño no nulo, por lo que algunos datos lograrán pasar, y al menos en mi sistema, puedo hacer que funcione hasta:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Más allá de eso, con
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Tenemos un punto muerto en el que nos encontramos en esta situación:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Hemos llenado las tuberías 3 y 6 (64 kB cada una). teeha leído ese byte extra, lo ha alimentado cmd1, pero
- ahora está bloqueado escribiendo en la tubería 3 mientras espera
cmd2vaciarlo
cmd2no puede vaciarlo porque está bloqueado escribiendo en la tubería 6, esperando catvaciarlo
cat no puede vaciarlo porque está esperando hasta que no haya más entradas en la tubería 5.
cmd1No puedo decir que catno hay más entrada porque está esperando más entrada de tee.
- y
teeno puedo decir que cmd1no hay más entradas porque está bloqueado ... y así sucesivamente.
Tenemos un bucle de dependencia y, por lo tanto, un punto muerto.
Ahora, ¿cuál es la solución? Las tuberías más grandes 3 y 4 (lo suficientemente grandes como para contener toda srcla salida) lo harían. Podríamos hacer eso, por ejemplo, insertando pv -qB 1Gentre teey cmd2/3dónde pvpodría almacenar hasta 1G de datos en espera cmd2y cmd3leerlos. Sin embargo, eso significaría dos cosas:
- eso está usando potencialmente mucha memoria y, además, duplicarlo
- eso no
cmd2logra que los 3 comandos cooperen porque en realidad solo comenzaría a procesar datos cuando cmd1 haya terminado.
Una solución al segundo problema sería hacer las tuberías 6 y 7 más grandes también. Suponiendo eso cmd2y cmd3produciendo tanta salida como consumen, eso no consumiría más memoria.
La única forma de evitar la duplicación de datos (en el primer problema) sería implementar la retención de datos en el despachador, es decir, implementar una variación teeque pueda alimentar los datos a la velocidad de salida más rápida (mantener los datos para alimentar el los más lentos a su propio ritmo). No es realmente trivial.
Entonces, al final, lo mejor que podemos obtener razonablemente sin programación es probablemente algo así como (sintaxis Zsh):
max_hold=1G
pee() (
n=0 ci= co= is=() os=()
for cmd do
if ((n)); then
eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
else
eval "coproc $cmd $ci $co"
fi
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c