Puede usar una combinación de GNU stdbuf y pee
de 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 fread
la entrada y fwrite
las 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
pclose
los tres comandos se encuentren secuencialmente.
Sobre cada uno pclose
, pee
vacía el búfer al comando y espera su finalización. Eso garantiza que mientras esos cmdx
comandos 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 pee
como 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 zsh
no 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, cmd1
comienza a masticar datos src
tan 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<&0
es evitar el hecho de que &
redirige stdin
desde /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 zsh
coproc:
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 cmd1
haya terminado. Y, como en el caso de lo tr b B
anterior, 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). tee
ha leído ese byte extra, lo ha alimentado cmd1
, pero
- ahora está bloqueado escribiendo en la tubería 3 mientras espera
cmd2
vaciarlo
cmd2
no puede vaciarlo porque está bloqueado escribiendo en la tubería 6, esperando cat
vaciarlo
cat
no puede vaciarlo porque está esperando hasta que no haya más entradas en la tubería 5.
cmd1
No puedo decir que cat
no hay más entrada porque está esperando más entrada de tee
.
- y
tee
no puedo decir que cmd1
no 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 src
la salida) lo harían. Podríamos hacer eso, por ejemplo, insertando pv -qB 1G
entre tee
y cmd2/3
dónde pv
podría almacenar hasta 1G de datos en espera cmd2
y cmd3
leerlos. Sin embargo, eso significaría dos cosas:
- eso está usando potencialmente mucha memoria y, además, duplicarlo
- eso no
cmd2
logra 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 cmd2
y cmd3
produciendo 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 tee
que 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