Dans le script bash, comment capturer stdout ligne par ligne

26

Dans un script bash, je voudrais capturer la sortie standard d'une longue ligne de commande ligne par ligne, afin qu'ils puissent être analysés et signalés pendant que la commande initiale est toujours en cours d'exécution. Voici la manière compliquée que j'imagine de le faire:

# 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

Je voudrais savoir s'il existe un moyen plus simple de le faire.

MODIFIER:

Afin de clarifier ma question, je vais ajouter ceci. Le longcommandaffiche son état ligne par ligne une fois par seconde. Je voudrais attraper la sortie avant la longcommandfin.

De cette façon, je peux potentiellement tuer le fichier longcommands'il ne fournit pas les résultats attendus.

J'ai essayé:

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

Mais whatever(par exemple echo) ne s'exécute qu'une fois longcommandterminé.

gfrigon
la source

Réponses:

32

Il suffit de diriger la commande dans une whileboucle. Il y a un certain nombre de nuances à cela, mais fondamentalement (dans bashou n'importe quel shell POSIX):

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

L'autre problème principal avec cela (autre que le IFScontenu ci-dessous) est lorsque vous essayez d'utiliser des variables de l'intérieur de la boucle une fois qu'elle est terminée. En effet, la boucle est en fait exécutée dans un sous-shell (juste un autre processus shell) à partir duquel vous ne pouvez pas accéder aux variables (elle se termine également lorsque la boucle le fait, à quel point les variables ont complètement disparu. Pour contourner ce problème, tu peux faire:

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

L'exemple de Hauke de la mise lastpipeen bashest une autre solution.

Mise à jour

Pour vous assurer que vous traitez la sortie de la commande «en temps réel», vous pouvez utiliser stdbufpour définir le processus « stdoutpour qu'il soit mis en mémoire tampon de ligne.

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

Cela configurera le processus pour écrire une ligne à la fois dans le canal au lieu de mettre en mémoire tampon interne sa sortie dans des blocs. Sachez que le programme peut modifier lui-même ce paramètre en interne. Un effet similaire peut être obtenu avec unbuffer(une partie de expect) ou script.

stdbufest disponible sur les systèmes GNU et FreeBSD, il n'affecte que la stdiomise en mémoire tampon et ne fonctionne que pour les applications non setuid, non setgid qui sont liées dynamiquement (car il utilise une astuce LD_PRELOAD).

Graeme
la source
@Stephane Le IFS=n'est pas nécessaire dans bash, j'ai vérifié cela après la dernière fois.
Graeme
2
Oui, ça l'est. Ce n'est pas nécessaire si vous omettez line(auquel cas le résultat est inséré $REPLYsans les espaces de début et de fin coupés). Essayez: echo ' x ' | bash -c 'read line; echo "[$line]"'et comparez avec echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'ouecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas
@Stephane, ok, je n'ai jamais réalisé qu'il y avait une différence entre cela et une variable nommée. Merci.
Graeme
@Graeme Je n'ai peut-être pas été clair dans ma question, mais j'aimerais traiter la sortie ligne par ligne avant la fin de la commande longue (afin de réagir rapidement si la commande longue affiche un message d'erreur). Je vais modifier ma question pour la rendre plus claire
gfrigon
@gfrigon, mis à jour.
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
la source