web-dev-qa-db-fra.com

Dans le script bash, comment capturer stdout ligne par ligne

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.

ÉDITER:

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

De cette façon, je peux potentiellement tuer le longcommand s'il ne fournit pas les résultats que j'attends.

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 longcommand terminé.

29
gfrigon

Il suffit de diriger la commande dans une boucle while. Il y a un certain nombre de nuances à cela, mais fondamentalement (dans bash ou tout shell POSIX):

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

L'autre gotcha principal avec cela (autre que les choses IFS 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, moment auquel 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 définir lastpipe dans bash est une autre solution.

Mise à jour

Pour vous assurer que vous traitez la sortie de la commande "en temps réel", vous pouvez utiliser stdbuf pour définir le processus "stdout pour 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 en blocs. Attention, le programme peut modifier lui-même ce paramètre en interne. Un effet similaire peut être obtenu avec unbuffer (partie de expect) ou script.

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

36
Graeme
#! /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
2
Hauke Laging