Bash: Itérer sur des lignes dans une variable

225

Comment peut-on correctement parcourir les lignes de bash soit dans une variable, soit à partir du résultat d'une commande? Définir simplement la variable IFS sur une nouvelle ligne fonctionne pour la sortie d'une commande, mais pas pour le traitement d'une variable contenant de nouvelles lignes.

Par exemple

#!/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

Cela donne la sortie:

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

Comme vous pouvez le constater, l'écho de la variable ou une itération sur la catcommande imprime correctement chacune des lignes. Cependant, la première boucle for imprime tous les éléments sur une seule ligne. Des idées?

Alex Spurling
la source
Juste un commentaire pour toutes les réponses: je devais faire $ (echo "$ line" | sed -e 's / ^ [[: space:]] * //') afin de découper le caractère de nouvelle ligne.
servermanfail

Réponses:

290

Avec bash, si vous souhaitez incorporer des nouvelles lignes dans une chaîne, entourez-la de la manière suivante $'':

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

Et si vous avez déjà une telle chaîne dans une variable, vous pouvez la lire ligne par ligne avec:

while read -r line; do
    echo "... $line ..."
done <<< "$list"
Glenn Jackman
la source
30
Juste une note qui done <<< "$list"est cruciale
Jason Axelson
21
La raison en done <<< "$list"est cruciale est que cela passera "$ list" comme entrée pourread
wisbucky
22
Je dois souligner ceci: la double citation $listest très cruciale.
André Chalella
8
echo "$list" | while ...pourrait paraître plus clair que... done <<< "$line"
kyb
5
Cette approche présente des inconvénients, car la boucle while s'exécutera dans un sous-shell: les variables affectées dans la boucle ne seront pas conservées.
Glenn Jackman
66

Vous pouvez utiliser while+ read:

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

Btw. l' -eoption to echoest non standard. Utilisez printfplutôt si vous voulez la portabilité.

maxelost
la source
12
Notez que si vous utilisez cette syntaxe, les variables affectées à l'intérieur de la boucle ne resteront pas après la boucle. Curieusement, la <<<version suggérée par glenn jackman ne fonctionne avec affectation variable.
Sparhawk
7
@Sparhawk Oui, c'est parce que le canal lance un sous-shell qui exécute la whilepièce. La <<<version ne le fait pas (du moins dans les nouvelles versions de bash).
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
personne important
la source
2
Ceci affiche tous les éléments, pas les lignes.
Tomasz Posłuszny
4
@ TomaszPosłuszny Non, cette opération génère 3 lignes (le nombre est de 3) sur ma machine. Notez le réglage de l'IFS. Vous pouvez également utiliser IFS=$'\n'pour le même effet.
Jmiserez
11

Voici une façon amusante de faire votre boucle:

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

Un peu plus sensible / lisible serait:

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

Mais c'est trop complexe, vous n'avez besoin que d'un espace:

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

Votre $linevariable ne contient pas de nouvelles lignes. Il contient des instances de \suivi de n. Vous pouvez voir cela clairement avec:

$ 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 substitution consiste à remplacer celles-ci par des espaces, ce qui est suffisant pour que cela fonctionne dans les boucles:

$ 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

Démo:

$ 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
Tapis
la source
Intéressant, pouvez-vous expliquer ce qui se passe ici? Il semblerait que vous remplaciez \ n par une nouvelle ligne ... Quelle est la différence entre la chaîne d'origine et la nouvelle?
Alex Spurling
@Alex: a mis à jour ma réponse - avec une version plus simple aussi :-)
Mat
J'ai essayé ceci et cela ne semble pas fonctionner si vous avez des espaces dans votre entrée. Dans l'exemple ci-dessus, nous avons "un \ nneux \ nnois" et cela fonctionne, mais cela échoue si nous avons "entrée un \ nentry deux \ nentry trois" car cela ajoute également une nouvelle ligne pour l'espace.
John Rocha
2
Juste une note qui, au lieu de définir, crpeut être utilisée $'\n'.
devios1
1

Vous pouvez également d'abord convertir la variable en un tableau, puis parcourir celui-ci.

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

Ce n'est utile que si vous ne voulez pas jouer avec le IFSet que vous avez également des problèmes avec la readcommande, comme cela peut arriver, si vous appelez un autre script dans la boucle, ce script peut vider votre tampon de lecture avant de revenir, comme cela m'est arrivé .

Torge
la source