Usar jq dentro de la cadena de tuberías no produce salida

12

El tema de la jqnecesidad de un filtro explícito cuando la salida se redirige se discute en toda la web. Pero no puedo redirigir la salida si jqes parte de una cadena de tuberías, incluso cuando se usa un filtro explícito.

Considerar:

touch in.txt
tail -f in.txt | jq '.f1'
# in a different terminal:
echo '{"f1":1,"f2":2}' >> in.txt
echo '{"f1":3,"f2":2}' >> in.txt

Como se esperaba, la salida en el terminal original del jqcomando es:

1
3

Pero si agrego algún tipo de redirección o tubería al final del jqcomando, la salida se silencia:

rm in.txt
touch in.txt
tail -f in.txt | jq '.f1' | tee out.txt
# in a different terminal:
echo '{"f1":1,"f2":2}' >> in.txt
echo '{"f1":3,"f2":2}' >> in.txt

No aparece ninguna salida en el primer terminal y out.txt está vacío.

He probado cientos de variaciones pero es un problema difícil de alcanzar. La única solución que he encontrado , como descubrí a través de mosquitto_subThe Things Network (que también descubrí el problema), es envolver las funciones tail y jq en un script de shell:

#!/bin/bash
tail -f $1 | while IFS='' read line; do
echo $line | jq '.f1'
done

Luego:

./tail_and_jq.sh | tee out.txt
# in a different terminal:
echo '{"f1":1,"f2":2}' >> in.txt
echo '{"f1":3,"f2":2}' >> in.txt

Y, efectivamente, aparece la salida:

1
3

Esto es con la última jqversión instalada a través de Homebrew:

$ echo $SHELL
/bin/bash
$ jq --version
jq-1.5
$ brew install jq
Warning: jq 1.5_3 is already installed and up-to-date

¿Es esto un error (en gran parte indocumentado) en jqmi comprensión de las cadenas de tuberías?

Heath Raftery
fuente
1
FWIW tiene una configuración bastante (bien, ligeramente) extraña aquí, que se utiliza tail -fpara proporcionar una entrada continua a un programa y teeprocesar la salida. Si todavía necesitara una respuesta, le habría sugerido que simplificara la cadena para <in.json jq '.f1' >out.jsonpoder reducir la causa.
David Z
Ver también BashFAQ # 9 - ¿Qué es el almacenamiento en búfer? O, ¿por qué mi línea de comando no produce resultados?tail -f logfile | grep 'foo bar' | awk ...
Charles Duffy
Todo un gran consejo para futuros esfuerzos, gracias. FWIW, el tailbit surgió de los esfuerzos por romper la tubería (ejecute el primer comando, coloque en T y redirija al archivo, siga esa cola, canalice al siguiente comando, redirija al archivo, etc.) y ejecútelo continuamente en secciones. Sin <embargo, es una buena herramienta para tener en cuenta.
Heath Raftery

Respuestas:

19

La salida de jqse almacena cuando se canaliza su salida estándar.

Para solicitar que jqvacíe su búfer de salida después de cada objeto, use su --unbufferedopción, por ej.

tail -f in.txt | jq --unbuffered '.f1' | tee out.txt

Del jqmanual:

--unbuffered

Vacíe la salida después de que se imprima cada objeto JSON (útil si está canalizando una fuente de datos lenta jqy jqla salida de la tubería en otro lugar).

Kusalananda
fuente
Además, la forma en que depuraría esto, para descubrir que el búfer de salida era el problema, suponiendo que simplemente no adivinaría eso, sería ejecutar la parte 'jq' bajo 'ltrace' y / o 'strace'. Sería obvio que está llamando a las funciones de salida de C stdio, pero no está llamando a la llamada al sistema write (2).
AnotherSmellyGeek
1
@AnotherSmellyGeek Posiblemente, o la utilidad de rastreo equivalente en nuestros Unices (tenga en cuenta que el OP está utilizando Homebrew, lo que significa que están en macOS, y yo estoy en OpenBSD, ninguno de los cuales tiene estas herramientas de Linux). Otra posibilidad es saber que el almacenamiento intermedio de salida puede ocurrir bajo ciertas circunstancias :-)
Kusalananda
Brillante. Y realmente aprecio todos los consejos para depurar esto en el futuro. El almacenamiento en búfer fue una de mis primeras dudas, pero el comportamiento diferente de las tuberías fue desconcertante para mis esfuerzos de depuración.
Heath Raftery
6

Lo que estás viendo aquí es el buffering de C stdio en acción. Almacenará la salida en un búfer hasta que alcance un cierto límite (puede ser 512 bytes, o 4KB o más) y luego lo enviará todo de una vez.

Este almacenamiento en búfer se deshabilita automáticamente si stdout está conectado a un terminal, pero cuando está conectado a una tubería (como en su caso), habilitará este comportamiento de almacenamiento en búfer.

La forma habitual de deshabilitar / controlar el almacenamiento en búfer es usar la setvbuf()función (consulte esta respuesta para obtener más detalles), pero eso debería hacerse en el código fuente de jqsí mismo, por lo que tal vez no sea algo práctico para usted ...

Hay una solución alternativa ... (Un truco, se podría decir). Hay un programa llamado "unbuffer", que se distribuye con "esperar" que puede crear un pseudo-terminal y conectarlo a un programa. Por lo tanto, aunque jqseguirá escribiendo en una tubería, pensará que está escribiendo en una terminal y se deshabilitará el efecto de almacenamiento en búfer.

Instale el paquete "expect", que debería venir con "unbuffer", si aún no lo tiene ... Por ejemplo, en Debian (o Ubuntu):

$ sudo apt-get install expect

Entonces puedes usar este comando:

$ tail -f in.txt | unbuffer -p jq '.f1' | tee out.txt

Consulte también esta respuesta para obtener más detalles sobre "unbuffer", y también puede encontrar una página de manual aquí .

filbranden
fuente
Me gusta que hayas explicado por qué ocurre el comportamiento observado, pero como señaló Kusalananda, jqimplementa de forma nativa la salida sin búfer, por lo que no hay necesidad de la solución.
David Z
Ah muy lindo! Empecé a buscar en la jqpágina de manual, pero me aburrí después de un tiempo y fui a hacer otras cosas ... ¡Es bueno saber que hay algo así! :-)
filbranden
1
Protip, vienen con GNU coreutils stdbuf -o0que inyectarán código a través de LD_PRELOAD y harán la setvbuf()llamada mágica por usted. Si funciona en macOS, no estoy seguro.
user1686
1
Mientras expectestá preinstalado en macos, unbufferno lo está. Sin embargo, es parte del paquete Homebrew, por lo que en macos brew install expectlo hará.
Heath Raftery