Bagaimana saya bisa membaca baris demi baris dari variabel dalam bash?

49

Saya memiliki variabel yang berisi output multiline dari suatu perintah. Apa cara paling efisien untuk membaca output baris demi baris dari variabel?

Sebagai contoh:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi
Eugene Yarmash
sumber

Jawaban:

65

Anda dapat menggunakan loop sementara dengan substitusi proses:

while read -r line
do
    echo "$line"
done < <(jobs)

Cara optimal untuk membaca variabel multiline adalah dengan menetapkan IFSvariabel kosong dan printfvariabel dengan baris baru:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

Catatan: Sesuai shellcheck sc2031 , penggunaan subtitusi proses lebih disukai daripada pipa untuk menghindari [secara halus] membuat subkulit.

Juga, harap menyadari bahwa dengan memberi nama variabel jobsitu dapat menyebabkan kebingungan karena itu juga nama perintah shell umum.

dogbane
sumber
3
Jika Anda ingin menyimpan semua spasi putih Anda, maka gunakan while IFS= read.... Jika Anda ingin mencegah \ interpretasi, maka gunakanread -r
Peter.O
Saya telah memperbaiki poin fred.bear yang disebutkan, serta diubah echomenjadi printf %s, sehingga skrip Anda akan berfungsi bahkan dengan input yang tidak jinak.
Gilles 'SANGAT berhenti menjadi jahat'
Untuk membaca dari variabel multiline, herestring lebih disukai daripada piping dari printf (lihat jawaban l0b0).
Atau
1
@atau Walaupun saya sudah cukup sering mendengar "disukai" ini, harus dicatat bahwa herestring selalu mengharuskan /tmpdirektori dapat ditulis, karena ini bergantung pada kemampuan untuk membuat file kerja sementara. Jika Anda menemukan diri Anda pada sistem terbatas dengan /tmphanya-baca (dan tidak dapat diubah oleh Anda), Anda akan senang tentang kemungkinan menggunakan solusi alternatif, misalnya dengan printfpipa.
syntaxerror
1
Dalam contoh kedua, jika variabel multi-baris tidak mengandung baris baru, Anda akan kehilangan elemen terakhir. Ubah ke:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett
26

Untuk memproses output baris perintah demi baris ( penjelasan ):

jobs |
while IFS= read -r line; do
  process "$line"
done

Jika Anda sudah memiliki data dalam variabel:

printf %s "$foo" | 

printf %s "$foo"hampir identik dengan echo "$foo", tetapi dicetak $foosecara harfiah, sedangkan echo "$foo"mungkin mengartikan $foosebagai opsi untuk perintah gema jika dimulai dengan -, dan mungkin memperluas urutan backslash di $foodalam beberapa shell.

Perhatikan bahwa dalam beberapa shell (abu, bash, pdksh, tetapi bukan ksh atau zsh), sisi kanan pipa berjalan dalam proses terpisah, sehingga variabel apa pun yang Anda atur dalam loop hilang. Misalnya, skrip penghitungan baris berikut mencetak 0 pada shell ini:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

Solusinya adalah dengan meletakkan sisa skrip (atau setidaknya bagian yang membutuhkan nilai dari $nloop) dalam daftar perintah:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

Jika bertindak pada baris yang tidak kosong cukup baik dan inputnya tidak besar, Anda dapat menggunakan pemisahan kata:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

Penjelasan: pengaturan IFSke baris baru tunggal membuat pemisahan kata hanya terjadi di baris baru (sebagai lawan dari karakter spasi putih di bawah pengaturan default). set -fmematikan globbing (yaitu ekspansi wildcard), yang jika tidak akan terjadi pada hasil substitusi perintah $(jobs)atau substitusi variabel $foo. The forLoop bekerja pada semua bagian $(jobs), yang semua garis non-kosong di output perintah. Terakhir, kembalikan globbing dan IFSpengaturan.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Saya mengalami masalah dengan pengaturan IFS dan unsetting IFS. Saya pikir hal yang benar untuk dilakukan adalah menyimpan nilai IFS lama dan mengatur IFS kembali ke nilai lama itu. Saya bukan ahli bash, tetapi dalam pengalaman saya, ini membuat Anda kembali ke bahavior asli.
Bjorn Roche
1
@BjornRoche: di dalam suatu fungsi, gunakan local IFS=something. Itu tidak akan mempengaruhi nilai lingkup global. IIRC, unset IFStidak membuat Anda kembali ke default (dan tentu saja tidak berfungsi jika itu bukan default sebelumnya).
Peter Cordes
14

Masalah: jika Anda menggunakan while, itu akan berjalan dalam subkulit dan semua variabel akan hilang. Solusi: gunakan untuk loop

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=
Dima
sumber
TERIMA KASIH BANYAK!! Semua solusi di atas gagal untuk saya.
hax0r_n_code
memipakan ke dalam while readloop di bash berarti loop sementara dalam subkulit, sehingga variabel tidak global. while read;do ;done <<< "$var"membuat loop body bukan subshell. (Bash baru-baru ini memiliki opsi untuk menempatkan tubuh cmd | whileloop tidak dalam subkulit, seperti yang selalu dimiliki ksh.)
Peter Cordes
Juga lihat posting terkait ini .
Wildcard
10

Dalam versi bash terbaru, gunakan mapfileatau readarrayuntuk secara efisien membaca output perintah ke dalam array

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

Penafian: contoh yang mengerikan, tetapi Anda dapat dengan cepat menghasilkan perintah yang lebih baik untuk digunakan daripada diri Anda sendiri

lihat
sumber
Ini cara yang bagus, tetapi mengotori / var / tmp dengan file sementara di sistem saya. +1
Eugene Yarmash
@Eugene: itu lucu. Sistem apa (distro / OS) yang aktif?
sehe
Ini FreeBSD 8. Cara mereproduksi: memasukkan readarrayfungsi dan memanggil fungsi beberapa kali.
Eugene Yarmash
Bagus, @sehe. +1
Teresa e Junior
7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Referensi:

l0b0
sumber
3
-rjuga merupakan ide bagus; Ini mencegah \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(yang sangat penting untuk mencegah kehilangan spasi putih)
Peter.O
-1

Anda dapat menggunakan <<< untuk membaca dari variabel yang berisi data yang dipisahkan dengan baris baru:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"
Matthew Herrmann
sumber
1
Selamat datang di Unix & Linux! Ini pada dasarnya menggandakan jawaban dari empat tahun lalu. Tolong jangan memposting jawaban kecuali Anda memiliki sesuatu yang baru untuk disumbangkan.
G-Man Mengatakan 'Reinstate Monica'