L'utilisation de jq dans la chaîne de tuyaux ne produit aucune sortie

12

La question d' jqavoir besoin d'un filtre explicite lorsque la sortie est redirigée est discutée partout sur le Web. Mais je ne peux pas rediriger la sortie si elle jqfait partie d'une chaîne de canalisations, même lorsqu'un filtre explicite est utilisé.

Considérer:

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

Comme prévu, la sortie dans le terminal d'origine de la jqcommande est:

1
3

Mais si j'ajoute une sorte de redirection ou de tuyauterie à la fin de la jqcommande, la sortie devient silencieuse:

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

Aucune sortie n'apparaît dans le premier terminal et out.txt est vide.

J'ai essayé des centaines de variantes mais c'est un problème difficile à cerner. La seule solution de contournement que j'ai trouvée , telle que découverte par le biais de mosquitto_subThe Things Network (où j'ai également découvert le problème), est d'envelopper les fonctions tail et jq dans un script shell:

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

Alors:

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

Et bien sûr, la sortie apparaît:

1
3

C'est avec la dernière version jqinstallée via 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

Est-ce un bug (largement non documenté) dans jqou avec ma compréhension des chaînes de tuyaux?

Heath Raftery
la source
1
FWIW vous avez ici une configuration assez (enfin, légèrement) étrange, utilisée tail -fpour fournir une entrée continue à un programme et teepour traiter la sortie. Si vous aviez encore besoin d'une réponse, j'aurais suggéré de simplifier la chaîne pour <in.json jq '.f1' >out.jsonque vous puissiez affiner la cause.
David Z
Voir aussi BashFAQ # 9 - Qu'esttail -f logfile | grep 'foo bar' | awk ...
Charles Duffy
Tous les bons conseils pour les efforts futurs, merci. FWIW, le tailbit est né des efforts pour casser le tuyau (exécutez la première commande, tee et rediriger vers le fichier, queue que, tuyau vers la commande suivante, rediriger vers le fichier, etc.) et l'exécuter en continu dans les sections. C'est <un bon outil à garder à l'esprit.
Heath Raftery

Réponses:

19

La sortie de jqest mise en mémoire tampon lorsque sa sortie standard est canalisée.

Pour demander de jqvider son tampon de sortie après chaque objet, utilisez son --unbufferedoption, par exemple

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

Du jqmanuel:

--unbuffered

Rincez la sortie après l'impression de chaque objet JSON (utile si vous canalisez une source de données lente dans jqet canalisez jqla sortie ailleurs).

Kusalananda
la source
De plus, la façon dont je déboguerais cela, afin de comprendre que la mise en mémoire tampon de sortie était le problème, en supposant que je ne devinerais pas simplement, serait d'exécuter la partie 'jq' sous 'ltrace' et / ou 'strace'. Il serait évident qu'il appelle des fonctions de sortie stdio C, mais pas l'appel syscall write (2).
AnotherSmellyGeek
1
@AnotherSmellyGeek Possible, ou l'utilitaire de traçage équivalent sur nos Unices (notez que l'OP utilise Homebrew, ce qui signifie qu'ils sont sur macOS, et je suis sur OpenBSD, aucun des deux ne disposant de ces outils Linux). Une autre possibilité est de savoir que la mise en mémoire tampon de sortie peut se produire dans certaines circonstances :-)
Kusalananda
Brillant. Et j'apprécie vraiment tous les conseils pour déboguer cela à l'avenir. La mise en mémoire tampon a été l'un de mes premiers doutes, mais le comportement différent pour la tuyauterie a déconcerté mes efforts de débogage.
Heath Raftery
6

Ce que vous voyez ici, c'est la mise en mémoire tampon C stdio en action. Il stockera la sortie sur un tampon jusqu'à ce qu'il atteigne une certaine limite (peut-être 512 octets, ou 4 Ko ou plus), puis enverra tout cela en même temps.

Cette mise en mémoire tampon est automatiquement désactivée si stdout est connecté à un terminal, mais lorsqu'il est connecté à un tuyau (comme dans votre cas), il active ce comportement de mise en mémoire tampon.

La façon habituelle de désactiver / contrôler la mise en mémoire tampon est d'utiliser la setvbuf()fonction (voir cette réponse pour plus de détails), mais cela devrait être fait dans le code source jqlui-même, donc peut-être pas quelque chose de pratique pour vous ...

Il y a une solution de contournement ... (Un hack, pourrait-on dire.) Il existe un programme appelé "unbuffer", qui est distribué avec "expect" qui peut créer un pseudo-terminal et le connecter à un programme. Ainsi, même s'il jqécrit toujours sur un canal, il pensera qu'il écrit sur un terminal et l'effet de mise en mémoire tampon sera désactivé.

Installez le paquet "expect", qui devrait venir avec "unbuffer", si vous ne l'avez pas déjà ... Par exemple, sur Debian (ou Ubuntu):

$ sudo apt-get install expect

Ensuite, vous pouvez utiliser cette commande:

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

Voir aussi cette réponse pour plus de détails sur "unbuffer", et vous pouvez trouver une page de manuel ici aussi .

filbranden
la source
J'aime que vous ayez expliqué pourquoi le comportement observé se produit, mais comme l'a souligné Kusalananda, jqimplémente nativement une sortie non tamponnée, il n'y a donc pas besoin de contourner ce problème.
David Z
Ah très sympa! J'ai commencé à chercher dans la jqpage de manuel mais je me suis ennuyé après un moment et je suis allé faire d'autres choses ... Bon de savoir qu'il y a quelque chose comme ça! :-)
filbranden
1
Protip, les coreutils GNU sont livrés avec stdbuf -o0qui injecteront du code via LD_PRELOAD et feront l' setvbuf()appel magique pour vous. Que cela fonctionne sur macOS, je ne suis pas sûr.
user1686
1
Bien qu'il expectsoit préinstallé sur macOS, ce unbuffern'est pas le cas. Cependant, il fait partie du package Homebrew, donc sur macos, brew install expectfera l'affaire.
Heath Raftery