W skrypcie bash, jak przechwycić standardowe wyjście linia po linii

26

W skrypcie bash chciałbym uchwycić standardowe dane wyjściowe długich linii poleceń po linii, aby można je było analizować i zgłaszać podczas działania polecenia początkowego. Oto skomplikowany sposób, w jaki mogę to sobie wyobrazić:

# Start long command in a separated process and redirect stdout to temp file
longcommand > /tmp/tmp$$.out &

#loop until process completes
ps cax | grep longcommand > /dev/null
while [ $? -eq 0 ]
do
    #capture the last lines in temp file and determine if there is new content to analyse
    tail /tmp/tmp$$.out

    # ...

    sleep 1 s  # sleep in order not to clog cpu

    ps cax | grep longcommand > /dev/null
done

Chciałbym wiedzieć, czy istnieje prostszy sposób na zrobienie tego.

EDYTOWAĆ:

Aby wyjaśnić moje pytanie, dodam to. longcommandWyświetla jego stan linia po linii raz na sekundę. Chciałbym uchwycić wynik przed longcommandukończeniem.

W ten sposób mogę potencjalnie zabić, longcommandjeśli nie przyniesie oczekiwanych rezultatów.

Próbowałem:

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Ale whatever(np. echo) Wykonuje się dopiero po longcommandzakończeniu.

gfrigon
źródło

Odpowiedzi:

32

Po prostu potokuj polecenie w whilepętli. Istnieje wiele niuansów, ale w zasadzie (w bashlub w dowolnej powłoce POSIX):

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Innym głównym gotcha z tym (innym niż te IFSponiżej) jest próba użycia zmiennych z pętli po jej zakończeniu. Wynika to z faktu, że pętla jest faktycznie wykonywana w podpowłoce (po prostu inny proces powłoki), z którego nie można uzyskać dostępu do zmiennych (również kończy się, gdy pętla się kończy, w którym to momencie zmienne całkowicie znikają. Aby to obejść, możesz to zrobić:

longcommand | {
  while IFS= read -r line
  do
    whatever "$line"
    lastline="$line"
  done

  # This won't work without the braces.
  echo "The last line was: $lastline"
}

Przykładem Hauke za ustawiania lastpipena bashinne rozwiązanie.

Aktualizacja

Aby upewnić się, że przetwarzane są dane wyjściowe polecenia „tak, jak to się dzieje”, można użyć stdbufdo ustawienia procesu stdoutna buforowanie linii.

stdbuf -oL longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Spowoduje to skonfigurowanie procesu do zapisywania jednego wiersza na raz w potoku zamiast wewnętrznego buforowania jego wyników w bloki. Uwaga: program może samodzielnie zmienić to ustawienie wewnętrznie. Podobny efekt można uzyskać za pomocą unbuffer(części expect) lub script.

stdbufjest dostępny w systemach GNU i FreeBSD, wpływa tylko na stdiobuforowanie i działa tylko w przypadku aplikacji nie-setuid, non-setgid, które są dynamicznie połączone (ponieważ używa sztuczki LD_PRELOAD).

Graeme
źródło
@Stephane Nie IFS=jest potrzebne bash, sprawdziłem to po raz ostatni.
Graeme
2
Tak to jest. Nie jest to konieczne, jeśli pominiesz line(w takim przypadku wynik jest wstawiany $REPLYbez przycinania spacji wiodących i końcowych). Spróbuj: echo ' x ' | bash -c 'read line; echo "[$line]"'i porównaj z echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'lubecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas
@Stephane, ok, nigdy nie zdawałem sobie sprawy, że istnieje różnica między tym a zmienną nazwaną. Dzięki.
Graeme
@Graeme Być może nie miałem jasności w swoim pytaniu, ale chciałbym przetworzyć linię wyjściową wiersz po wierszu przed zakończeniem długiej komendy (aby szybko zareagować, jeśli długie komendy wyświetlą komunikat o błędzie).
Zmodyfikuję
@gfrigon, zaktualizowano.
Graeme
2
#! /bin/bash
set +m # disable job control in order to allow lastpipe
shopt -s lastpipe
longcommand |
  while IFS= read -r line; do lines[i]="$line"; ((i++)); done
echo "${lines[1]}" # second line
Hauke ​​Laging
źródło