¿Cómo puedes diferenciar dos tuberías en Bash?

143

¿Cómo puede diferenciar dos canales sin usar archivos temporales en Bash? Digamos que tiene dos canales de comando:

foo | bar
baz | quux

Y desea encontrar el diffen sus salidas. Una solución obviamente sería:

foo | bar > /tmp/a
baz | quux > /tmp/b
diff /tmp/a /tmp/b

¿Es posible hacerlo sin el uso de archivos temporales en Bash? Puede deshacerse de un archivo temporal al conectar una de las tuberías para diferenciar:

foo | bar > /tmp/a
baz | quux | diff /tmp/a -

Pero no puede canalizar ambas tuberías en diff simultáneamente (al menos no de manera obvia). ¿Hay algún truco inteligente que implique /dev/fdhacer esto sin usar archivos temporales?

Adam Rosenfield
fuente

Respuestas:

146

Una línea con 2 archivos tmp (no lo que quieres) sería:

 foo | bar > file1.txt && baz | quux > file2.txt && diff file1.txt file2.txt

Con bash , puedes intentarlo:

 diff <(foo | bar) <(baz | quux)

 foo | bar | diff - <(baz | quux)  # or only use process substitution once

La segunda versión le recordará más claramente qué entrada fue cuál, mostrando
-- /dev/stdinvs. ++ /dev/fd/63o algo, en lugar de dos fds numerados.


Ni siquiera aparecerá una canalización con nombre en el sistema de archivos, al menos en los sistemas operativos donde bash puede implementar la sustitución de procesos mediante el uso de nombres de archivo como /dev/fd/63para obtener un nombre de archivo que el comando puede abrir y leer para leer realmente desde un descriptor de archivo ya abierto que bash establece antes de ejecutar el comando. (es decir, bash usa pipe(2)antes de fork, y luego dup2para redirigir desde la salida quuxa un descriptor de archivo de entrada para diff, en fd 63.)

En un sistema sin "mágico" /dev/fdo /proc/self/fd, bash podría usar canalizaciones con nombre para implementar la sustitución del proceso, pero al menos las administraría por sí mismo, a diferencia de los archivos temporales, y sus datos no se escribirían en el sistema de archivos.

Puede verificar cómo bash implementa la sustitución de procesos echo <(true)para imprimir el nombre del archivo en lugar de leerlo. Se imprime /dev/fd/63en un sistema Linux típico. O para obtener más detalles sobre exactamente qué llamadas al sistema usa bash, este comando en un sistema Linux rastreará las llamadas al sistema de archivos y descriptores de archivos

strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'

Sin bash, podrías hacer una pipa con nombre . Use -para indicar diffque lea una entrada de STDIN y use la tubería nombrada como la otra:

mkfifo file1_pipe.txt
foo|bar > file1_pipe.txt && baz | quux | diff file1_pipe.txt - && rm file1_pipe.txt

Tenga en cuenta que solo puede canalizar una salida a múltiples entradas con el comando tee:

ls *.txt | tee /dev/tty txtlist.txt 

El comando anterior muestra la salida de ls * .txt al terminal y la envía al archivo de texto txtlist.txt.

Pero con la sustitución de procesos, puede usar teepara alimentar los mismos datos en múltiples canales:

cat *.txt | tee >(foo | bar > result1.txt)  >(baz | quux > result2.txt) | foobar
VonC
fuente
55
incluso sin golpe, puedes usar un mkfifo a; cmd >a& cmd2|diff a -; rm a
juego
Se puede utilizar un tubo regular para uno de los argumentos: pipeline1 | diff -u - <(pipeline2). Luego, la salida le recordará más claramente qué entrada fue cuál, mostrando -- /dev/stdinvs. ++ /dev/fd/67o algo, en lugar de dos fds numerados.
Peter Cordes
Process Substitution ( foo <( pipe )) no modifica el sistema de archivos. La pipa es anónima ; no tiene nombre en el sistema de archivos . El shell utiliza la pipellamada del sistema para crearlo, no mkfifo. Utilícelo strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'para rastrear llamadas de sistema de archivo y descriptor de archivo si desea verlo usted mismo. En Linux, /dev/fd/63es parte del /procsistema de archivos virtual; automáticamente tiene entradas para cada descriptor de archivo, y no es una copia de los contenidos. Así que no puede llamar a eso un "archivo temporal" a menos que foo 3<bar.txtcuente
Peter Cordes
@PeterCordes Buenos puntos. He incluido tu comentario en la respuesta para mayor visibilidad.
VonC
1
@PeterCordes Te dejaré cualquier edición: eso es lo que hace que Stack Overflow sea interesante: cualquiera puede "arreglar" una respuesta.
VonC
127

En bash puede usar subshells, para ejecutar las tuberías de comando individualmente, encerrando la tubería entre paréntesis. A continuación, puede prefijarlos con <para crear canalizaciones con nombre anónimo que luego puede pasar a diff.

Por ejemplo:

diff <(foo | bar) <(baz | quux)

Las canalizaciones con nombre anónimo son administradas por bash, por lo que se crean y destruyen automáticamente (a diferencia de los archivos temporales).

BenM
fuente
1
Mucho más detallado que mi redacción sobre la misma solución, lote anónimo. +1
VonC
44
Esto se llama sustitución de proceso en Bash.
Franklin Yu
5

Algunas personas que llegan a esta página podrían estar buscando una diferencia línea por línea, para lo cual commogrep -f deberían usarse en su lugar.

Una cosa a destacar es que, en todos los ejemplos de respuestas, las diferencias no comenzarán realmente hasta que ambas transmisiones hayan terminado. Pruebe esto con, por ejemplo:

comm -23 <(seq 100 | sort) <(seq 10 20 && sleep 5 && seq 20 30 | sort)

Si esto es un problema, puede probar sd (stream diff), que no requiere la clasificación (como lo commhace) ni la sustitución del proceso como en los ejemplos anteriores, es órdenes o magnitud más rápido quegrep -f y admite flujos infinitos.

El ejemplo de prueba que propongo se escribiría así en sd:

seq 100 | sd 'seq 10 20 && sleep 5 && seq 20 30'

Pero la diferencia es que seq 100se diferenciaría de seq 10inmediato. Tenga en cuenta que, si una de las transmisiones es untail -f , la diferencia no se puede hacer con la sustitución del proceso.

Aquí hay una entrada de blog que escribí sobre diffing corrientes en el terminal, que introduce sd.

mlg
fuente