¿Cómo puedo leer línea por línea de una variable en bash?


49

Tengo una variable que contiene la salida multilínea de un comando. ¿Cuál es la forma más eficiente de leer la salida línea por línea de la variable?

Por ejemplo:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi

Respuestas:


65

Puede usar un ciclo while con sustitución de proceso:

while read -r line
do
    echo "$line"
done < <(jobs)

Una forma óptima de leer una variable multilínea es establecer una IFSvariable en blanco y printfla variable con una nueva línea final:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

Nota: Según shellcheck sc2031 , el uso de la sustitución de procesos es preferible a una tubería para evitar [sutilmente] crear una subshell.

Además, tenga en cuenta que al nombrar la variable jobspuede causar confusión, ya que ese también es el nombre de un comando de shell común.


3
Si desea mantener todo su espacio en blanco, use while IFS= read... Si desea evitar la interpretación, useread -r
Peter

He arreglado los puntos mencionados por fred.bear, así como cambiado echoa printf %s, para que su script funcione incluso con entradas no mansas.
Gilles 'SO- deja de ser malvado'

Para leer desde una variable multilínea, una herestring es preferible a la tubería de printf (vea la respuesta de l0b0).
ata

1
@ata Aunque he escuchado que esto es "preferible" con bastante frecuencia, debe tenerse en cuenta que una herejía siempre requiere que el /tmpdirectorio se pueda escribir, ya que se basa en poder crear un archivo de trabajo temporal. Si alguna vez te encuentras en un sistema restringido con /tmpsolo lectura (y no cambiable por ti), estarás contento con la posibilidad de usar una solución alternativa, por ejemplo, con la printftubería.
syntaxerror

1
En el segundo ejemplo, si la variable de varias líneas no contiene una nueva línea final, perderá el último elemento. Cámbielo a:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett

26

Para procesar la salida de un comando línea por línea ( explicación ):

jobs |
while IFS= read -r line; do
  process "$line"
done

Si ya tiene los datos en una variable:

printf %s "$foo" | 

printf %s "$foo"es casi idéntico echo "$foo", pero se imprime $fooliteralmente, mientras que echo "$foo"podría interpretarse $foocomo una opción para el comando echo si comienza con un -, y podría expandir las secuencias de barra invertida $fooen algunos shells.

Tenga en cuenta que en algunos shells (ash, bash, pdksh, pero no ksh o zsh), el lado derecho de una tubería se ejecuta en un proceso separado, por lo que se pierde cualquier variable que establezca en el bucle. Por ejemplo, el siguiente script de conteo de líneas imprime 0 en estos shells:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

Una solución alternativa es colocar el resto del script (o al menos la parte que necesita el valor del $nbucle) en una lista de comandos:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

Si actuar sobre las líneas no vacías es lo suficientemente bueno y la entrada no es enorme, puede usar la división de palabras:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

Explicación: establecer IFSuna nueva línea hace que la división de palabras se produzca solo en las nuevas líneas (en oposición a cualquier carácter de espacio en blanco en la configuración predeterminada). set -fdesactiva el globbing (es decir, la expansión de comodines), que de otro modo ocurriría con el resultado de una sustitución de comando $(jobs)o un sustitutino variable $foo. El forbucle actúa sobre todas las piezas de $(jobs), que son todas las líneas no vacías en la salida del comando. Finalmente, restaure el globbing y la IFSconfiguración.


He tenido problemas para configurar IFS y desarmar IFS. Creo que lo correcto es almacenar el valor anterior de IFS y volver a establecer IFS en ese valor anterior. No soy un experto en bash, pero en mi experiencia, esto te lleva de vuelta al comportamiento original.
Bjorn Roche

1
@BjornRoche: dentro de una función, use local IFS=something. No afectará el valor del alcance global. IIRC, unset IFSno lo devuelve al valor predeterminado (y ciertamente no funciona si no era el valor predeterminado de antemano).
Peter Cordes

14

Problema: si usa el bucle while, se ejecutará en subshell y se perderán todas las variables. Solución: usar para bucle

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=

¡¡MUCHAS GRACIAS!! Todas las soluciones anteriores fallaron para mí.
hax0r_n_code

canalizar en un while readbucle en bash significa que el bucle while está en una subshell, por lo que las variables no son globales. while read;do ;done <<< "$var"hace que el cuerpo del bucle no sea una subshell. (Bash reciente tiene una opción para poner el cuerpo de un cmd | whilebucle no en una subcapa, como ksh siempre ha tenido.)
Peter Cordes


10

En versiones recientes de bash, use mapfileo readarraypara leer eficientemente la salida del comando en matrices

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

Descargo de responsabilidad: un ejemplo horrible, pero puede encontrar un mejor comando para usar que usted mismo


Es una buena manera, pero camadas / var / tmp con archivos temporales en mi sistema. +1 de todos modos
Eugene Yarmash

@eugene: eso es gracioso. ¿En qué sistema (distro / OS) está eso?
Sehe

Es FreeBSD 8. Cómo reproducir: pon readarrayuna función y llama a la función varias veces.
Eugene Yarmash

Buena, @sehe. +1
Teresa e Junior

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Referencias


3
-res una buena idea también; Previene \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(que es esencial para evitar la pérdida de espacios en blanco)
Peter.O

-1

Puede usar <<< para simplemente leer la variable que contiene los datos separados por nueva línea:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"

1
¡Bienvenido a Unix y Linux! Esto esencialmente duplica una respuesta de hace cuatro años. No publique una respuesta a menos que tenga algo nuevo que aportar.
G-Man dice 'Restablece a Mónica'
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.