Bash: Iterazione su righe in una variabile


225

Come si fa a scorrere correttamente su righe in bash in una variabile o dall'output di un comando? La semplice impostazione della variabile IFS su una nuova riga funziona per l'output di un comando ma non durante l'elaborazione di una variabile che contiene nuove righe.

Per esempio

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

Questo dà l'output:

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

Come puoi vedere, l'eco della variabile o l'iterazione sul catcomando stampa ciascuna delle righe una per una correttamente. Tuttavia, il primo per il ciclo stampa tutti gli elementi su una sola riga. Qualche idea?


Solo un commento per tutte le risposte: ho dovuto fare $ (echo "$ line" | sed -e 's / ^ [[: space:]] * //') per tagliare il carattere di nuova riga.
servermanfail

Risposte:


290

Con bash, se si desidera incorporare nuove righe in una stringa, racchiudere la stringa con $'':

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

E se hai già una stringa del genere in una variabile, puoi leggerla riga per riga con:

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

30
Solo una nota che done <<< "$list"è cruciale
Jason Axelson il

21
Il motivo done <<< "$list"è fondamentale perché questo passerà "$ list" come input perread
wisbucky

22
Devo sottolineare questo: IL DOPPIO PREVENTIVO $listè molto cruciale.
André Chalella,

8
echo "$list" | while ...potrebbe apparire più chiaro di... done <<< "$line"
kyb

5
Ci sono aspetti negativi di questo approccio poiché il ciclo while verrà eseguito in una subshell: eventuali variabili assegnate nel ciclo non persisteranno.
Glenn Jackman,

66

Puoi usare while+ read:

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

Btw. l' -eopzione per echonon è standard. Utilizzare printfinvece, se si desidera la portabilità.


12
Si noti che se si utilizza questa sintassi, le variabili assegnate all'interno del loop non si attaccheranno dopo il loop. Stranamente, la <<<versione suggerita da Glenn Jackman non funziona con assegnazione variabile.
Sparhawk,

7
@Sparhawk Sì, è perché la pipe avvia una subshell eseguendo la whileparte. La <<<versione no (almeno nelle nuove versioni bash).
massimo

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
Ciò genera tutti gli elementi, non le righe.
Tomasz Posłuszny,

4
@ TomaszPosłuszny No, questo genera 3 (il conteggio è 3) righe sulla mia macchina. Notare l'impostazione dell'IFS. Puoi anche usare IFS=$'\n'per lo stesso effetto.
jmiserez,

11

Ecco un modo divertente di fare il tuo ciclo for:

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

Un po 'più sensibile / leggibile sarebbe:

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

Ma è troppo complesso, hai solo bisogno di uno spazio lì dentro:

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

La tua $linevariabile non contiene newline. Contiene casi \seguiti da n. Puoi vederlo chiaramente 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 sostituzione sta sostituendo quelli con spazi, il che è sufficiente per far funzionare i loop:

$ 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

demo:

$ 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

Interessante, puoi spiegare cosa sta succedendo qui? Sembra che tu stia sostituendo \ n con una nuova riga ... Qual è la differenza tra la stringa originale e quella nuova?
Alex Spurling,

@Alex: aggiornata la mia risposta - anche con una versione più semplice :-)
Mat

Ho provato questo e non sembra funzionare se hai spazi nel tuo input. Nell'esempio sopra abbiamo "one \ ntwo \ nthree" e questo funziona ma fallisce se abbiamo "entry one \ nentry two \ nentry tre" in quanto aggiunge anche una nuova riga per lo spazio.
John Rocha,

2
Solo una nota che invece di definire crè possibile utilizzare $'\n'.
devios1,

1

Puoi anche prima convertire la variabile in un array, quindi scorrere su questo.

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

Questo è utile solo se non vuoi fare confusione con IFSe hai anche problemi con il readcomando, come può accadere, se chiami un altro script all'interno del ciclo che quello script può svuotare il tuo buffer di lettura prima di tornare, come è successo a me .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.