Bash: iterando sobre líneas en una variable


225

¿Cómo se itera correctamente sobre las líneas en bash, ya sea en una variable o desde la salida de un comando? Simplemente establecer la variable IFS en una nueva línea funciona para la salida de un comando, pero no cuando se procesa una variable que contiene nuevas líneas.

Por ejemplo

#!/bin/bash

list="One\ntwo\nthree\nfour"

#Print the list with echo
echo -e "echo: \n$list"

#Set the field separator to new line
IFS=$'\n'

#Try to iterate over each line
echo "For loop:"
for item in $list
do
        echo "Item: $item"
done

#Output the variable to a file
echo -e $list > list.txt

#Try to iterate over each line from the cat command
echo "For loop over command output:"
for item in `cat list.txt`
do
        echo "Item: $item"
done

Esto da la salida:

echo: 
One
two
three
four
For loop:
Item: One\ntwo\nthree\nfour
For loop over command output:
Item: One
Item: two
Item: three
Item: four

Como puede ver, hacer eco de la variable o iterar sobre el catcomando imprime cada una de las líneas correctamente una por una. Sin embargo, el primer bucle for imprime todos los elementos en una sola línea. ¿Algunas ideas?


Solo un comentario para todas las respuestas: tuve que hacer $ (echo "$ line" | sed -e 's / ^ [[: space:]] * //') para recortar el carácter de nueva línea.
servermanfail

Respuestas:


290

Con bash, si desea incrustar nuevas líneas en una cadena, encierre la cadena con $'':

$ list="One\ntwo\nthree\nfour"
$ echo "$list"
One\ntwo\nthree\nfour
$ list=$'One\ntwo\nthree\nfour'
$ echo "$list"
One
two
three
four

Y si ya tiene una cadena de este tipo en una variable, puede leerla línea por línea con:

while read -r line; do
    echo "... $line ..."
done <<< "$list"

30
Solo una nota de que done <<< "$list"es crucial
Jason Axelson

21
La razón done <<< "$list"es crucial porque eso pasará "$ list" como la entrada aread
wisbucky

22
Necesito enfatizar esto: las COTIZACIONES DOBLES $listson muy cruciales.
André Chalella

8
echo "$list" | while ...podría parecer más claro que... done <<< "$line"
kyb

55
Hay desventajas en ese enfoque, ya que el ciclo while se ejecutará en una subshell: las variables asignadas en el ciclo no persistirán.
Glenn Jackman

66

Puedes usar while+ read:

some_command | while read line ; do
   echo === $line ===
done

Por cierto. La -eopción echono es estándar. Use en su printflugar, si desea portabilidad.


12
Tenga en cuenta que si usa esta sintaxis, las variables asignadas dentro del bucle no se pegarán después del bucle. Curiosamente, la <<<versión sugerida por Glenn Jackman no funciona con la asignación de variables.
Sparhawk

77
@Sparhawk Sí, eso se debe a que la tubería inicia una subshell que ejecuta la whilepieza. La <<<versión no lo hace (en nuevas versiones de bash, al menos).
maxelost

26
#!/bin/sh

items="
one two three four
hello world
this should work just fine
"

IFS='
'
count=0
for item in $items
do
  count=$((count+1))
  echo $count $item
done

2
Esto genera todos los elementos, no líneas.
Tomasz Posłuszny

44
@ TomaszPosłuszny No, esto genera 3 líneas (el recuento es 3) en mi máquina. Tenga en cuenta la configuración del IFS. También podría usar IFS=$'\n'para el mismo efecto.
jmiserez

11

Aquí hay una forma divertida de hacer tu bucle for:

for item in ${list//\\n/
}
do
   echo "Item: $item"
done

Un poco más sensible / legible sería:

cr='
'
for item in ${list//\\n/$cr}
do
   echo "Item: $item"
done

Pero eso es demasiado complejo, solo necesitas un espacio allí:

for item in ${list//\\n/ }
do
   echo "Item: $item"
done

Usted $linevariable no contiene saltos de línea. Contiene instancias de \seguido por n. Puedes ver eso claramente con:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo $list | hexdump -C

$ ./t.sh
00000000  4f 6e 65 5c 6e 74 77 6f  5c 6e 74 68 72 65 65 5c  |One\ntwo\nthree\|
00000010  6e 66 6f 75 72 0a                                 |nfour.|
00000016

La sustitución está reemplazando aquellos con espacios, lo cual es suficiente para que funcione en bucles:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C

$ ./t.sh 
00000000  4f 6e 65 20 74 77 6f 20  74 68 72 65 65 20 66 6f  |One two three fo|
00000010  75 72 0a                                          |ur.|
00000013

Manifestación:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C
for item in ${list//\\n/ } ; do
    echo $item
done

$ ./t.sh 
00000000  4f 6e 65 20 74 77 6f 20  74 68 72 65 65 20 66 6f  |One two three fo|
00000010  75 72 0a                                          |ur.|
00000013
One
two
three
four

Interesante, ¿puedes explicar lo que está pasando aquí? Parece que estás reemplazando \ n con una nueva línea ... ¿Cuál es la diferencia entre la cadena original y la nueva?
Alex Spurling

@Alex: actualicé mi respuesta - con una versión más simple también :-)
Mat

Probé esto y no parece funcionar si tienes espacios en tu entrada. En el ejemplo anterior tenemos "one \ ntwo \ nthree" y esto funciona pero falla si tenemos "entry one \ nentry two \nentry three", ya que también agrega una nueva línea para el espacio.
John Rocha

2
Solo una nota que en lugar de definir crpuedes usar $'\n'.
devios1

1

También puede convertir primero la variable en una matriz, luego iterar sobre esto.

lines="abc
def
ghi"

declare -a theArray

while read -r line
do
    theArray+=($line)            
done <<< "$lines"

for line in "${theArray[@]}"
do
    echo "$line"
    #Do something complex here that would break your read loop
done

Esto solo es útil si no desea meterse con el comando IFSy también tiene problemas con el readcomando, ya que puede suceder, si llama a otro script dentro del bucle, ese script puede vaciar su búfer de lectura antes de regresar, como me sucedió a mí .

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.