O uso de jq na cadeia de tubos não produz saída

12

A questão da jqnecessidade de um filtro explícito quando a saída é redirecionada é discutida em toda a web. Mas não consigo redirecionar a saída se fizer jqparte de uma cadeia de tubulação, mesmo quando um filtro explícito estiver em uso.

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 esperado, a saída no terminal original do jqcomando é:

1
3

Mas se eu adicionar qualquer tipo de redirecionamento ou canalização ao final do jqcomando, a saída ficará silenciosa:

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

Nenhuma saída aparece no primeiro terminal e o out.txt está vazio.

Eu tentei centenas de variações, mas é uma questão ilusória. A única solução alternativa que encontrei , como foi descoberta na mosquitto_subThe Things Network (onde também descobri o problema), é agrupar as funções tail e jq em um script de shell:

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

Então:

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

E com certeza, a saída aparece:

1
3

Isto é com o mais recente jqinstalado 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

Isso é um bug (em grande parte não documentado) no jqou com meu entendimento das correntes de tubulação?

Heath Raftery
fonte
1
FWIW, você tem uma configuração bastante (bem, um pouco) estranha aqui, usando tail -fpara fornecer entrada contínua a um programa e teeprocessar a saída. Se você ainda precisasse de uma resposta, eu sugeriria simplificar a cadeia para <in.json jq '.f1' >out.jsonque você pudesse restringir o que está causando isso.
David Z
Veja também BashFAQ # 9 - O que é buffer? Ou, por que minha linha de comando não produz saída:tail -f logfile | grep 'foo bar' | awk ...
Charles Duffy
Todos os ótimos conselhos para esforços futuros, obrigado. FWIW, a parte tailsurgiu dos esforços para quebrar o canal (execute o primeiro comando, tee e redirecione para arquivo, siga-o, canalize para o próximo comando, redirecione para arquivo, etc.) e execute-o continuamente em seções. A <é uma boa ferramenta para se ter em mente.
Heath Raftery

Respostas:

19

A saída de jqé armazenada em buffer quando sua saída padrão é canalizada.

Para solicitar que jqlibere seu buffer de saída após cada objeto, use sua --unbufferedopção, por exemplo

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

Do jqmanual:

--unbuffered

Liberte a saída após a impressão de cada objeto JSON (útil se você estiver canalizando uma fonte de dados lenta para a saída da jqtubulação em jqoutro local).

Kusalananda
fonte
Além disso, a maneira como eu depurava isso, para descobrir que o buffer de saída era o problema, supondo que eu simplesmente não acharia isso, seria executar a parte 'jq' em 'ltrace' e / ou 'strace'. Seria óbvio que está chamando as funções de saída C stdio, mas não chamando o syscall write (2).
AnotherSmellyGeek
1
@AnotherSmellyGeek Possivelmente, ou o utilitário de rastreamento equivalente em nossos Unices (observe que o OP está usando o Homebrew, o que significa que eles estão no macOS, e eu estou no OpenBSD, nenhum dos quais possui essas ferramentas Linux). Outra possibilidade é só sei que o buffer de saída pode acontecer em determinadas circunstâncias :-)
Kusalananda
Brilhante. E realmente aprecio todos os conselhos sobre depuração disso no futuro. O buffer foi uma das minhas primeiras dúvidas, mas o comportamento diferente da tubulação estava atrapalhando meus esforços de depuração.
Heath Raftery
6

O que você está vendo aqui é o buffer do stdio em ação. Ele armazena a saída em um buffer até atingir um determinado limite (pode ser 512 bytes ou 4KB ou maior) e depois envia tudo de uma só vez.

Esse buffer é desativado automaticamente se o stdout estiver conectado a um terminal, mas quando estiver conectado a um pipe (como no seu caso), ele permitirá esse comportamento de buffer.

A maneira usual de desativar / controlar o buffer é usar a setvbuf()função (consulte esta resposta para obter mais detalhes), mas isso precisa ser feito no próprio código-fonte jq, então talvez não seja algo prático para você ...

Existe uma solução alternativa ... (Um hack, pode-se dizer.) Há um programa chamado "buffer", distribuído com "expect", que pode criar um pseudo-terminal e conectá-lo a um programa. Portanto, mesmo que jqainda esteja gravando em um canal, ele pensará que está gravando em um terminal e o efeito de buffer será desativado.

Instale o pacote "expect", que deve vir com "unbuffer", se você ainda não o possui ... Por exemplo, no Debian (ou Ubuntu):

$ sudo apt-get install expect

Então você pode usar este comando:

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

Veja também esta resposta para obter mais detalhes sobre "buffer", e você pode encontrar uma página de manual aqui também .

filbranden
fonte
Gosto que você tenha explicado por que o comportamento observado ocorre, mas, como Kusalananda apontou, jqimplementa nativamente a saída sem buffer, para que não haja necessidade de solução alternativa.
David Z
Ah muito legal! Comecei a procurar na jqpágina de manual, mas fiquei entediado depois de um tempo e fui fazer outras coisas ... É bom saber que há algo assim! :
Fil
1
Protip, os coreutils GNU vêm com os stdbuf -o0quais injetam código via LD_PRELOAD e fazem a setvbuf()chamada mágica para você. Se funciona no macOS, não tenho certeza.
usar o seguinte comando
1
Enquanto expectestiver pré-instalado no macos, unbuffernão é. No entanto, faz parte do pacote Homebrew, assim como nos macos brew install expect.
Heath Raftery