Bash: Iterating over lines dalam suatu variabel

225

Bagaimana cara iterate yang benar pada baris di bash baik dalam variabel, atau dari output perintah? Cukup mengatur variabel IFS ke baris baru berfungsi untuk output dari perintah tetapi tidak ketika memproses variabel yang berisi baris baru.

Sebagai contoh

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

Ini memberikan 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

Seperti yang Anda lihat, menggemakan variabel atau mengulangi catperintah mencetak setiap baris satu per satu dengan benar. Namun, yang pertama untuk loop mencetak semua item dalam satu baris. Ada ide?

Alex Spurling
sumber
Hanya komentar untuk semua jawaban: Saya harus melakukan $ (echo "$ line" | sed -e 's / ^ [[: space:]] * //') untuk memangkas karakter baris baru.
servermanfail

Jawaban:

290

Dengan bash, jika Anda ingin menyematkan baris baru dalam sebuah string, lampirkan string dengan $'':

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

Dan jika Anda sudah memiliki string dalam variabel, Anda dapat membacanya baris demi baris dengan:

while read -r line; do
    echo "... $line ..."
done <<< "$list"
glenn jackman
sumber
30
Hanya sebuah catatan bahwa done <<< "$list"itu sangat penting
Jason Axelson
21
Alasannya done <<< "$list"sangat penting adalah karena itu akan lulus "$ list" sebagai masukan untukread
wisbucky
22
Saya perlu menekankan ini: QUOTING-QUOTING $listsangat penting.
André Chalella
8
echo "$list" | while ...bisa tampak lebih jelas daripada... done <<< "$line"
kyb
5
Ada kelemahan pada pendekatan itu karena loop sementara akan berjalan dalam subkulit: variabel apa pun yang ditetapkan dalam loop tidak akan bertahan.
glenn jackman
66

Anda dapat menggunakan while+ read:

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

Btw. yang -epilihan untuk echonon-standar. Gunakan printfsebagai gantinya, jika Anda menginginkan portabilitas.

maksimal
sumber
12
Perhatikan bahwa jika Anda menggunakan sintaks ini, variabel yang ditetapkan di dalam loop tidak akan menempel setelah loop. Anehnya, <<<versi yang disarankan oleh glenn jackman tidak bekerja dengan variabel tugas.
Sparhawk
7
@Sparhawk Ya, itu karena pipa memulai subkulit yang mengeksekusi whilebagian tersebut. The <<<Versi tidak (dalam versi pesta baru, setidaknya).
maksimal
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
tidak ada yang penting
sumber
2
Ini menampilkan semua item, bukan garis.
Tomasz Posłuszny
4
@ TomaszPosłuszny Tidak, ini menghasilkan 3 (hitungan adalah 3) baris pada mesin saya. Perhatikan pengaturan IFS. Anda juga bisa menggunakan IFS=$'\n'untuk efek yang sama.
jmiserez
11

Berikut cara lucu untuk melakukan for loop Anda:

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

Sedikit lebih masuk akal / mudah dibaca adalah:

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

Tapi itu terlalu rumit, Anda hanya perlu ruang di sana:

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

Anda $linevariabel tidak mengandung baris. Ini berisi contoh \diikuti oleh n. Anda dapat melihatnya dengan jelas dengan:

$ 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

Substitusi ini menggantikan yang memiliki spasi, yang cukup untuk membuatnya bekerja untuk 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
Tikar
sumber
Menarik, dapatkah Anda menjelaskan apa yang sedang terjadi di sini? Sepertinya Anda mengganti \ n dengan baris baru ... Apa perbedaan antara string asli dan yang baru?
Alex Spurling
@Alex: memperbarui jawaban saya - dengan versi yang lebih sederhana juga :-)
Mat
Saya mencoba ini dan tampaknya tidak berfungsi jika Anda memiliki ruang di input Anda. Dalam contoh di atas, kita memiliki "satu \ ntwo \ ntiga" dan ini berfungsi tetapi gagal jika kita memiliki "entri satu \ ndua dua \ ndua tiga" karena juga menambahkan baris baru untuk ruang juga.
John Rocha
2
Hanya sebuah catatan bahwa alih-alih mendefinisikan crAnda dapat menggunakan $'\n'.
devios1
1

Anda juga dapat pertama-tama mengonversi variabel menjadi array, kemudian beralih di atasnya.

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

Ini hanya berguna jika Anda tidak ingin mengacaukannya IFSdan juga memiliki masalah dengan readperintah, karena itu bisa terjadi, jika Anda memanggil skrip lain di dalam loop itu skrip itu dapat mengosongkan buffer baca Anda sebelum kembali, seperti yang terjadi pada saya .

Torge
sumber