In Bash-Skript, wie stdout Zeile für Zeile erfasst wird

26

In einem Bash-Skript möchte ich die Standardausgabe eines langen Befehls zeilenweise erfassen, damit sie analysiert und gemeldet werden können, während der erste Befehl noch ausgeführt wird. Das ist die komplizierte Art, wie ich es mir vorstellen kann:

# 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

Ich würde gerne wissen, ob es einen einfacheren Weg gibt.

BEARBEITEN:

Um meine Frage zu klären, werde ich dies hinzufügen. Der longcommandzeigt seinen Status zeilenweise einmal pro Sekunde an. Ich möchte die Ausgabe vor dem longcommandAbschluss abfangen .

Auf diese Weise kann ich möglicherweise die töten, longcommandwenn es nicht die Ergebnisse liefert, die ich erwarte.

Ich habe versucht:

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

Aber whatever(zB echo) wird erst ausgeführt, nachdem longcommandabgeschlossen wurde.

gfrigon
quelle

Antworten:

32

Leiten Sie den Befehl einfach in eine whileSchleife. Es gibt eine Reihe von Nuancen, aber im Grunde (in bashoder jeder POSIX-Shell):

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

Das andere Hauptproblem IFSist, wenn Sie versuchen, Variablen innerhalb der Schleife zu verwenden, sobald diese beendet ist. Dies liegt daran, dass die Schleife tatsächlich in einer Sub-Shell (nur einem anderen Shell-Prozess) ausgeführt wird, von der aus Sie nicht auf Variablen zugreifen können. du kannst tun:

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"
}

Hauke des Beispiel der Einstellung lastpipein basheiner anderen Lösung.

Aktualisieren

Um sicherzustellen, dass Sie die Ausgabe des Befehls "wie sie ausgeführt wird" verarbeiten, können Sie stdbufden Prozess so einstellen stdout, dass er zeilengepuffert wird.

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

Dadurch wird der Prozess so konfiguriert, dass jeweils eine Zeile in die Pipe geschrieben wird, anstatt die Ausgabe intern in Blöcke zu puffern. Beachten Sie, dass das Programm diese Einstellung intern selbst ändern kann. Ein ähnlicher Effekt kann mit unbuffer(einem Teil von expect) oder erzielt werden script.

stdbufist auf GNU- und FreeBSD-Systemen verfügbar, wirkt sich nur auf die stdioPufferung aus und funktioniert nur für nicht-setuid- und nicht-setgid-Anwendungen, die dynamisch verknüpft sind (da ein LD_PRELOAD-Trick verwendet wird).

Graeme
quelle
@Stephane Der IFS=wird nicht benötigt bash, das habe ich nach dem letzten mal überprüft.
Graeme
2
ja ist es. Es wird nicht benötigt, wenn Sie weglassen line(in diesem Fall wird das Ergebnis $REPLYohne die vorangestellten und nachfolgenden Leerzeichen eingefügt). Versuchen Sie: echo ' x ' | bash -c 'read line; echo "[$line]"'und vergleichen Sie mit echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'oderecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas
@Stephane, ok, ich habe nie bemerkt, dass es einen Unterschied zwischen dieser und einer benannten Variablen gibt. Vielen Dank.
Graeme
@Graeme In meiner Frage war ich mir vielleicht nicht sicher, aber ich möchte die Ausgabe vor Abschluss des Langbefehls zeilenweise verarbeiten (um schnell zu reagieren, wenn im Langbefehl eine Fehlermeldung angezeigt wird). Ich werde meine Frage bearbeiten, um es klarer zu machen
gfrigon
@gfrigon, aktualisiert.
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
quelle