Użycie jq w łańcuchu rur nie daje żadnych wyników

12

Kwestia jqpotrzeby jawnego filtrowania podczas przekierowywania danych wyjściowych jest omawiana w Internecie. Ale nie jestem w stanie przekierować danych wyjściowych, jeśli jqjest częścią łańcucha potoków, nawet jeśli używany jest filtr jawny.

Rozważać:

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

Zgodnie z oczekiwaniami dane wyjściowe w oryginalnym terminalu z jqpolecenia to:

1
3

Ale jeśli dodam jakiekolwiek przekierowanie lub potokowanie na końcu jqpolecenia, dane wyjściowe zostaną wyciszone:

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

Brak danych wyjściowych w pierwszym terminalu i out.txt jest pusty.

Próbowałem setek odmian, ale jest to zagadka nieuchwytna. Jedynym obejściem, jakie znalazłem , jak odkryłem za pomocą mosquitto_subThe Things Network (w którym również odkryłem problem), jest zawinięcie funkcji tail i jq w skrypcie powłoki:

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

Następnie:

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

I rzeczywiście, pojawia się wynik:

1
3

Jest to najnowszy jqzainstalowany przez 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

Czy jest to (w dużej mierze nieudokumentowany) błąd w jqlub z moim rozumieniem łańcuchów rur?

Heath Raftery
źródło
1
FWIW masz tutaj dość (dobrze, nieco) dziwną konfigurację, używając tail -fdo ciągłego wprowadzania danych do programu i teeprzetwarzania danych wyjściowych. Jeśli nadal potrzebujesz odpowiedzi, sugerowałbym uproszczenie łańcucha <in.json jq '.f1' >out.json, abyś mógł zawęzić jego przyczyny.
David Z
Zobacz także BashFAQ # 9 - Co to jest buforowanie? Albo dlaczego moja linia poleceń nie generuje danych wyjściowych:tail -f logfile | grep 'foo bar' | awk ...
Charles Duffy
Wszystkie wspaniałe porady dotyczące przyszłych wysiłków, dziękuję. FWIW, tailbit powstał z wysiłków mających na celu rozbicie potoku (uruchomienie pierwszego polecenia, tee i przekierowanie do pliku, zakończenie tego, potok do następnego polecenia, przekierowanie do pliku itp.) I ciągłe uruchamianie go w sekcjach. <Jest dobrym narzędziem, aby pamiętać jednak.
Heath Raftery,

Odpowiedzi:

20

Wyjście z jqjest buforowane, gdy przesyłane jest standardowe wyjście.

Aby zażądać jqopróżnienia bufora wyjściowego po każdym obiekcie, użyj jego --unbufferedopcji, np

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

Z jqinstrukcji:

--unbuffered

Opróżnij dane wyjściowe po wydrukowaniu każdego obiektu JSON (przydatne, jeśli podłączasz powolne źródło danych do, jqa dane jqwyjściowe w innym miejscu).

Kusalananda
źródło
Ponadto sposobem, w jaki to debugowałem, aby dowiedzieć się, że buforowanie danych wyjściowych było problemem, przy założeniu, że po prostu nie zgadnę, byłoby uruchomienie części „jq” pod „ltrace” i / lub „strace”. Byłoby oczywiste, że wywołuje funkcje wyjściowe C stdio, ale nie wywołuje funkcji systemowej write (2).
AnotherSmellyGeek
1
@AnotherSmellyGeek Prawdopodobnie, lub równoważne narzędzie do śledzenia w naszych Unices (zauważ, że OP używa Homebrew, co oznacza, że ​​są one na macOS, a ja jestem na OpenBSD, żadne z nich nie ma tych narzędzi Linuksa). Inną możliwością jest po prostu wiedzieć, że buforowanie danych wyjściowych może się zdarzyć w pewnych okolicznościach :-)
Kusalananda
Znakomity. I naprawdę doceniam wszystkie porady dotyczące debugowania tego w przyszłości. Buforowanie było jednym z moich pierwszych wątpliwości, ale odmienne zachowanie dla pipingu flummoxowało moje wysiłki debugowania.
Heath Raftery,
6

To, co tu widzisz, to buforowanie stdio w akcji. Będzie przechowywać dane wyjściowe w buforze, dopóki nie osiągnie określonego limitu (może wynosić 512 bajtów lub 4KB lub więcej), a następnie wyśle ​​je wszystkie naraz.

Buforowanie to wyłącza się automatycznie, jeśli stdout jest podłączony do terminala, ale gdy jest podłączony do potoku (jak w twoim przypadku), włącza to zachowanie buforowania.

Zwykłym sposobem wyłączania / kontrolowania buforowania jest użycie setvbuf()funkcji ( więcej informacji znajduje się w tej odpowiedzi ), ale należy to zrobić w samym kodzie źródłowym jq, więc może nie jest to dla ciebie praktyczne ...

Istnieje obejście ... (włamanie, można powiedzieć.) Istnieje program o nazwie „unbuffer”, który jest dystrybuowany wraz z „expect”, który może stworzyć pseudo-terminal i połączyć go z programem. Tak więc, mimo że jqnadal będzie zapisywać do potoku, pomyśli, że zapisuje do terminala, a efekt buforowania zostanie wyłączony.

Zainstaluj pakiet „expect”, który powinien zawierać „unbuffer”, jeśli jeszcze go nie masz ... Na przykład na Debianie (lub Ubuntu):

$ sudo apt-get install expect

Następnie możesz użyć tego polecenia:

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

Zobacz także tę odpowiedź, aby uzyskać więcej informacji na temat „unbuffer”, możesz też znaleźć tutaj stronę podręcznika man .

filbranden
źródło
Podoba mi się, że wyjaśniłeś, dlaczego występuje obserwowane zachowanie, ale jak zauważył Kusalananda, jqnatywnie implementuje niebuforowane dane wyjściowe, więc nie ma potrzeby obejścia tego problemu.
David Z
Ach bardzo miło! Zacząłem przeglądać jqstronę podręcznika, ale po chwili się nudziłem i zacząłem robić inne rzeczy ... Dobrze wiedzieć, że jest coś takiego! :-)
filbranden
1
Protip, GNU coreutils, stdbuf -o0które dostarczają kod przez LD_PRELOAD i wykonują setvbuf()magiczne wezwanie dla ciebie. Czy to działa w systemie macOS, nie jestem pewien.
user1686
1
Chociaż expectjest wstępnie zainstalowany na komputerach Mac, unbuffernie jest. Jest to jednak część pakietu Homebrew, więc brew install expectwystarczy na Macach .
Heath Raftery,