Mengapa `sementara IFS = read` sering digunakan, alih-alih` IFS =; saat membaca..`?

81

Tampaknya praktik normal akan menempatkan pengaturan IFS di luar loop sementara agar tidak mengulangi pengaturan untuk setiap iterasi ... Apakah ini hanya gaya "monyet lihat, monyet lakukan" seperti kebiasaan, untuk monyet ini sampai Saya membaca man read , atau apakah saya melewatkan beberapa perangkap yang halus (atau sangat jelas) di sini?

Peter.O
sumber

Jawaban:

82

Perangkapnya adalah itu

IFS=; while read..

mengatur IFSuntuk seluruh lingkungan shell di luar loop, sedangkan

while IFS= read

mendefinisikan ulang hanya untuk readdoa (kecuali di shell Bourne). Anda dapat memeriksa apakah melakukan loop seperti

while IFS= read xxx; ... done

kemudian setelah loop tersebut, echo "blabalbla $IFS ooooooo"dicetak

blabalbla
 ooooooo

sedangkan sesudahnya

IFS=; read xxx; ... done

yang IFS tetap didefinisikan ulang: sekarang echo "blabalbla $IFS ooooooo"cetakan

blabalbla  ooooooo

Jadi, jika Anda menggunakan bentuk kedua, Anda harus ingat untuk me-reset: IFS=$' \t\n'.


Bagian kedua dari pertanyaan ini telah digabungkan di sini , jadi saya telah menghapus jawaban terkait dari sini.

rozcietrzewiacz
sumber
Oke, sepertinya 'perangkap' potensial adalah untuk mengabaikan untuk mereset IFS luar ... Tapi saya bertanya-tanya apakah ada sesuatu yang sedang terjadi ... Saya sedang menguji hal-hal di sini, cukup tergesa-gesa, dan saya sudah perhatikan bahwa pengaturan IFS di daftar perintah while berperilaku qute berbeda, tergantung apakah diikuti oleh titik dua atau tidak. Saya tidak mengerti perilaku ini (belum), dan sekarang saya bertanya-tanya apakah ada pertimbangan khusus yang terlibat pada tingkat ini ... misalnya. while IFS=X readtidak terpecah pada X, tetapi while IFS=X; readtidak ...
Peter.O
(Maksud Anda semi- kolon, kan?) Yang kedua whiletidak masuk akal - kondisi untuk while ujung pada titik koma itu, jadi tidak ada loop aktual ... readmenjadi hanya perintah pertama di dalam loop satu elemen ... Atau tidak ? Bagaimana kalau dobegitu ..?
rozcietrzewiacz
1
Tidak, tunggu - Anda benar, Anda dapat memiliki beberapa perintah dalam whilekondisi (sebelum do).
rozcietrzewiacz
Oh .. pasti, Anda dapat memilikinya ... seperti yang telah Anda sadari ... tetapi mereka tampaknya tidak menyukai semi-colon ... (dan loop akan terus looping ad-infinitum sampai perintah terakhir mengembalikan -zero kode keluar) ... Saya sekarang bertanya-tanya apakah jebakan terletak di sektor yang berbeda sama sekali; yang memahami bagaimana daftar perintah sementara bekerja, mis. mengapa IFS=bekerja, tetapi IFS=Xtidak ... (atau mungkin saya sudah melakukan ini untuk sementara waktu .. coffee break diperlukan :)
Peter.O
1
$ rozcietrzewiacz .. Ups ... Saya tidak melihat pembaruan Anda, ketika saya memindahkan pembaruan saya (sebagaimana disebutkan dalam komentar sebelumnya) .. Itu terlihat menarik, dan itu mulai masuk akal ... tetapi bahkan untuk malam- burung seperti saya, sudah sangat terlambat ... (Saya baru mendengar burung pagi:) ... Yang mengatakan, saya telah mengumpulkan sedikit dan membaca contoh Anda ... Saya pikir saya sudah mendapatkannya, sebenarnya saya ' Saya yakin Anda sudah mendapatkannya, tapi saya harus tidur :) ... Ini hampir Eureka! saat ... terima kasih
Peter.O
45

Mari kita lihat contoh, dengan beberapa teks input yang dibuat dengan cermat:

text=' hello  world\
foo\bar'

Itu dua baris, yang pertama dimulai dengan spasi dan diakhiri dengan garis miring terbalik. Pertama, mari kita lihat apa yang terjadi tanpa tindakan pencegahan di sekitarnya read(tetapi gunakan printf '%s\n' "$text"untuk mencetak dengan hati-hati $texttanpa risiko ekspansi). (Di bawah, $ ‌adalah prompt shell.)

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

readmemakan backslash: backslash-newline menyebabkan baris baru diabaikan, dan backslash-apa pun mengabaikan backslash pertama. Untuk menghindari backslash diperlakukan secara khusus, kami menggunakan read -r.

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

Itu lebih baik, kami memiliki dua garis seperti yang diharapkan. Dua baris hampir berisi konten yang diinginkan: ruang ganda di antara hellodan worldtelah dipertahankan, karena berada dalam linevariabel. Di sisi lain, ruang awal dimakan. Itu karena readmembaca banyak kata saat Anda meneruskan variabel, kecuali bahwa variabel terakhir berisi sisa baris - tetapi masih dimulai dengan kata pertama, yaitu spasi awal dibuang.

Jadi, untuk membaca setiap baris secara harfiah, kita perlu memastikan bahwa tidak ada pemisahan kata yang terjadi. Kami melakukan ini dengan mengatur IFSvariabel ke nilai kosong.

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

Perhatikan bagaimana kami mengatur IFS secara spesifik untuk durasi readbuilt-in . The IFS= read -r lineset variabel lingkungan IFS(untuk nilai kosong) khusus untuk pelaksanaan read. Ini adalah turunan dari sintaks perintah sederhana umum : urutan penugasan variabel (kemungkinan kosong) diikuti oleh nama perintah dan argumennya (juga, Anda bisa melempar pengalihan kapan saja). Karena readbuilt-in, variabel tidak pernah benar-benar berakhir di lingkungan proses eksternal; namun nilai dari $IFSapa yang kami tetapkan di sana selama readeksekusi long . Perhatikan bahwa readitu bukan built-in khusus , sehingga tugas tidak hanya berlangsung selama durasinya.

Karenanya kami berhati-hati untuk tidak mengubah nilai IFSinstruksi lain yang mungkin bergantung padanya. Kode ini akan berfungsi tidak peduli apa kode yang ditetapkan di IFSawal, dan tidak akan menimbulkan masalah jika kode di dalam loop bergantung IFS.

Berbeda dengan potongan kode ini, yang mencari file di jalur yang dipisahkan titik dua. Daftar nama file dibaca dari file, satu nama file per baris.

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

Jika loop itu while IFS=; read -r name; do …, maka for dir in $PATHtidak akan terpecah $PATHmenjadi komponen yang dipisahkan oleh titik dua. Jika kode itu IFS=; while read …, akan lebih jelas lagi bahwa IFStidak diatur ke :dalam loop body.

Tentu saja, akan mungkin untuk mengembalikan nilai IFSsetelah dieksekusi read. Tetapi itu akan membutuhkan mengetahui nilai sebelumnya, yang merupakan upaya ekstra. IFS= readadalah cara sederhana (dan, mudah, juga cara terpendek).

¹ Dan, jika readdiinterupsi oleh sinyal yang terperangkap, mungkin saat jebakan mengeksekusi - ini tidak ditentukan oleh POSIX dan tergantung pada shell dalam praktiknya.

Gilles
sumber
4
Terima kasih Gilles .. tur yang dipandu sangat bagus .. (maksud Anda 'set -f'?) .... Sekarang, untuk pembaca, untuk menyatakan kembali apa yang telah dikatakan, saya ingin menekankan masalah yang telah saya melihatnya dengan cara yang salah. Pertama dan terpenting adalah kenyataan bahwa konstruk while IFS= read(tanpa tanda titik koma =) bukanlah bentuk khusus dari whileatau IFSatau read.. Konstruk itu generik: yaitu. anyvar=anyvalue anycommand. Kurangnya ;setelah pengaturan anyvarmembuat lingkup anyvar lokal menjadi anycommand.. Loop sementara - lakukan / dilakukan adalah 100% tidak terkait dengan lingkup lokal any_var.
Peter.O
3

Terlepas dari (sudah diklarifikasi) IFSperbedaan scoping antara while IFS='' read, IFS=''; while readdan while IFS=''; readidiom (per-perintah vs script / shell-lebar IFSvariabel scoping), pelajaran dibawa pulang adalah bahwa Anda kehilangan terkemuka dan spasi dari garis masukan jika variabel IFS diatur ke (mengandung a) ruang.

Ini dapat memiliki konsekuensi yang cukup serius jika jalur file sedang diproses.

Oleh karena itu pengaturan variabel IFS ke string kosong sama sekali bukan ide yang buruk karena hal itu memastikan bahwa spasi spasi awal dan akhir garis tidak dilucuti.

Lihat juga: Bash, baca baris demi baris dari file, dengan IFS

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
jon
sumber
+1 demonstrasi yang sangat baik, pembersihan setelah dengan 'rm * file * dengan * spasi *'
amdn
0

Terinspirasi oleh jawaban Yuzem

Jika Anda ingin mengatur IFSke karakter yang sebenarnya, ini bekerja untuk saya

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
Steven Penny
sumber