En script bash, cómo capturar stdout línea por línea

26

En un script bash, me gustaría capturar la salida estándar de un comando largo línea por línea, para que puedan analizarse e informarse mientras el comando inicial aún se está ejecutando. Esta es la forma complicada que puedo imaginar de hacerlo:

# 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

Me gustaría saber si hay una manera más sencilla de hacerlo.

EDITAR:

Para aclarar mi pregunta, agregaré esto. El longcommandmuestra su estado línea por línea una vez por segundo. Me gustaría captar la salida antes de que se longcommandcomplete.

De esta manera, potencialmente puedo matar longcommandsi no proporciona los resultados que espero.

Yo he tratado:

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

Pero whatever(por ejemplo echo) solo se ejecuta después de longcommandcompletarse.

gfrigon
fuente
askubuntu.com/questions/344407/…
Trevor Boyd Smith el

Respuestas:

32

Simplemente canalice el comando en un whilebucle. Hay una serie de matices en esto, pero básicamente (en basho cualquier shell POSIX):

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

El otro problema principal con esto (aparte de las IFScosas a continuación) es cuando intentas usar variables desde dentro del ciclo una vez que ha terminado. Esto se debe a que el bucle se ejecuta realmente en un sub-shell (solo otro proceso de shell) desde el que no puede acceder a las variables (también finaliza cuando el bucle lo hace, en cuyo punto las variables se han ido por completo. Para evitar esto, tu puedes hacer:

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

Ejemplo de configuración de Hauke lastpipeen bashotra solución.

Actualizar

Para asegurarse de que está procesando la salida del comando 'como sucede', puede usar stdbufpara configurar el proceso ' stdoutpara que esté en línea.

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

Esto configurará el proceso para escribir una línea a la vez en la tubería en lugar de almacenar internamente su salida en bloques. Tenga en cuenta que el programa puede cambiar esta configuración internamente. Se puede lograr un efecto similar con unbuffer(parte de expect) o script.

stdbufestá disponible en los sistemas GNU y FreeBSD, solo afecta el stdioalmacenamiento en búfer y solo funciona para aplicaciones no setuid, no setgid que están vinculadas dinámicamente (ya que utiliza un truco LD_PRELOAD).

Graeme
fuente
@Stephane The IFS=no es necesario bash, lo comprobé después de la última vez.
Graeme
2
sí lo es. No es necesario si omite line(en cuyo caso el resultado se coloca $REPLYsin los espacios iniciales y finales recortados). Prueba: echo ' x ' | bash -c 'read line; echo "[$line]"'y compara con echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'oecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas
@Stephane, ok, nunca me di cuenta de que había una diferencia entre eso y una variable con nombre. Gracias.
Graeme
@Graeme Puede que no haya sido claro en mi pregunta, pero me gustaría procesar la salida línea por línea antes de que se complete el comando largo (para reaccionar rápidamente si los comandos largos muestran un mensaje de error).
Editaré
@gfrigon, actualizado.
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
fuente