Bash: Iterowanie po liniach w zmiennej

225

Jak poprawnie iterować po liniach w bash albo w zmiennej, albo w wyniku polecenia? Po prostu ustawienie zmiennej IFS na nową linię działa na wyjściu polecenia, ale nie podczas przetwarzania zmiennej zawierającej nowe linie.

Na przykład

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

Daje to wynik:

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

Jak widać, echo zmiennej lub iteracja nad catpoleceniem wypisuje kolejno każdą z linii. Jednak pierwsza dla pętli drukuje wszystkie elementy w jednym wierszu. Jakieś pomysły?

Alex Spurling
źródło
Tylko komentarz do wszystkich odpowiedzi: Musiałem zrobić $ (echo „$ line” | sed -e 's / ^ [[: spacja:]] * //'), aby przyciąć znak nowej linii.
servermanfail

Odpowiedzi:

290

W przypadku bash, jeśli chcesz osadzić znaki nowego wiersza, dołącz ciąg znaków $'':

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

A jeśli masz już taki ciąg w zmiennej, możesz czytać go wiersz po wierszu za pomocą:

while read -r line; do
    echo "... $line ..."
done <<< "$list"
Glenn Jackman
źródło
30
Tylko uwaga, że done <<< "$list"jest to kluczowe
Jason Axelson
21
Powód done <<< "$list"jest kluczowy, ponieważ przekazuje „$ list” jako dane wejściowe doread
mądrego szczęścia
22
Muszę to podkreślić: PODWÓJNE CYTOWANIE $listjest bardzo ważne.
André Chalella,
8
echo "$list" | while ...może wydawać się wyraźniejszy niż... done <<< "$line"
kyb
5
Takie podejście ma swoje wady, ponieważ pętla while będzie działać w podpowłoce: żadne zmienne przypisane w pętli nie zostaną zachowane.
glenn jackman
66

Możesz użyć while+ read:

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

Btw. -eopcja echojest niestandardowa. printfZamiast tego użyj , jeśli chcesz mieć przenośność.

maxelost
źródło
12
Zauważ, że jeśli użyjesz tej składni, zmienne przypisane wewnątrz pętli nie będą się trzymać po pętli. Co dziwne, <<<wersja sugerowane przez Glenn Jackman ma pracować ze zmienną zadania.
Sparhawk
7
@Sparhawk Tak, ponieważ potok uruchamia podpowłokę wykonującą whileczęść. <<<Wersja nie robi (w nowych wersjach bash, przynajmniej).
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
nikt ważny
źródło
2
Wyprowadza to wszystkie elementy, a nie wiersze.
Tomasz Posłuszny
4
@ TomaszPosłuszny Nie, to daje 3 (liczę 3) wierszy na moim komputerze. Zwróć uwagę na ustawienie IFS. Możesz również użyć IFS=$'\n'tego samego efektu.
jmiserez
11

Oto zabawny sposób wykonania pętli for:

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

Trochę bardziej sensownym / czytelnym byłoby:

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

Ale to wszystko jest zbyt skomplikowane, potrzebujesz tylko miejsca:

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

Twoja $linezmienna nie zawiera znaków nowej linii. Zawiera przypadki, \po których następuje n. Możesz to wyraźnie zobaczyć dzięki:

$ 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

Podstawienie polega na zamianie spacji, co wystarcza, aby zadziałało w przypadku pętli:

$ 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

Próbny:

$ 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
Mata
źródło
Ciekawe, czy możesz wyjaśnić, co się tutaj dzieje? Wygląda na to, że zastępujesz \ n nową linią ... Jaka jest różnica między oryginalnym ciągiem a nowym?
Alex Spurling
@Alex: zaktualizowałem moją odpowiedź - również prostszą wersją :-)
Mat
Wypróbowałem to i wydaje się, że nie działa, jeśli masz spacje w danych wejściowych. W powyższym przykładzie mamy „one \ ntwo \ nthree” i to działa, ale to się nie powiedzie, jeśli mamy „entry one \ nentry two \ nentry three”, ponieważ dodaje również nową linię dla spacji.
John Rocha,
2
Tylko uwaga, że ​​zamiast definiować cr, możesz użyć $'\n'.
devios1,
1

Możesz także najpierw przekonwertować zmienną na tablicę, a następnie wykonać iterację.

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

Przydaje się to tylko wtedy, gdy nie chcesz zadzierać, IFSa także masz problemy z readpoleceniem, co może się zdarzyć, jeśli wywołasz inny pętlę w pętli, że ten skrypt może opróżnić bufor odczytu przed powrotem, tak jak to się stało .

Torge
źródło