Memahami “IFS = read -r line”

60

Saya jelas mengerti bahwa seseorang dapat menambahkan nilai ke variabel pemisah bidang internal. Sebagai contoh:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Saya juga mengerti bahwa read -r lineakan menyimpan data dari stdinke variabel bernama line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Namun, bagaimana suatu perintah dapat memberikan nilai variabel? Dan apakah pertama-tama menyimpan data dari stdinke variabel linedan kemudian memberikan nilai lineke IFS?

Martin
sumber

Jawaban:

104

Beberapa orang memiliki gagasan keliru yang readmerupakan perintah untuk membaca sebuah baris. Ini bukan.

readmembaca kata-kata dari garis (mungkin garis miring terbalik), di mana kata-kata $IFSdibatasi dan garis miring terbalik dapat digunakan untuk menghindari pembatas (atau melanjutkan garis).

Sintaks generik adalah:

read word1 word2... remaining_words

readmembaca stdin satu byte pada suatu waktu sampai menemukan karakter newline tidak lolos (atau end-of-input), membagi bahwa menurut aturan yang kompleks dan menyimpan hasil membelah yang ke $word1, $word2... $remaining_words.

Misalnya pada input seperti:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

dan dengan nilai default $IFS, read a b cakan menetapkan:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Sekarang jika hanya melewati satu argumen, itu tidak menjadi read line. Itu masih read remaining_words. Pemrosesan backslash masih dilakukan, karakter spasi IFS masih dihapus dari awal dan akhir.

The -rpilihan menghilangkan pengolahan backslash. Jadi, perintah yang sama di atas dengan -rsebaliknya akan menetapkan

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Sekarang, untuk bagian pemisahan, penting untuk menyadari bahwa ada dua kelas karakter untuk $IFS: karakter spasi IFS (yaitu spasi dan tab (dan baris baru, meskipun di sini itu tidak masalah kecuali jika Anda menggunakan -d), yang juga terjadi berada di nilai default $IFS) dan yang lainnya. Perlakuan untuk dua kelas karakter berbeda.

Dengan IFS=:( :menjadi tidak karakter spasi IFS), masukan seperti :foo::bar::akan dipecah menjadi "", "foo", "", bardan ""(dan tambahan ""dengan beberapa implementasi meskipun itu tidak masalah kecuali read -a). Sementara jika kita menggantinya :dengan spasi, pemisahan dilakukan hanya menjadi foodan bar. Yang memimpin dan yang tertinggal diabaikan, dan urutannya diperlakukan seperti satu. Ada aturan tambahan saat karakter spasi dan non-spasi putih digabungkan $IFS. Beberapa implementasi dapat menambah / menghapus perlakuan khusus dengan menggandakan karakter di IFS ( IFS=::atau IFS=' ').

Jadi di sini, jika kita tidak ingin karakter spasi putih terkemuka dan tertinggal dilucuti, kita perlu menghapus karakter spasi putih IFS dari IFS.

Bahkan dengan karakter IFS-non-spasi putih, jika baris input berisi satu (dan hanya satu) karakter tersebut dan itu adalah karakter terakhir dalam baris (seperti IFS=: read -r wordpada input seperti foo:) dengan cangkang POSIX (bukan zshatau beberapa pdkshversi), input tersebut dianggap sebagai satu fookata karena dalam cangkang itu, karakter $IFSdianggap sebagai terminator , jadi wordakan berisi foo, bukan foo:.

Jadi, cara kanonik untuk membaca satu jalur input dengan readbuiltin adalah:

IFS= read -r line

(perhatikan bahwa untuk sebagian besar readimplementasi, yang hanya berfungsi untuk baris teks karena karakter NUL tidak didukung kecuali dalam zsh).

Menggunakan var=value cmdsintaks memastikan IFShanya diatur secara berbeda selama durasi cmdperintah itu.

Catatan sejarah

The readbuiltin diperkenalkan oleh Bourne shell dan sudah membaca kata-kata , bukan baris. Ada beberapa perbedaan penting dengan cangkang POSIX modern.

Shell Bourne readtidak mendukung -ropsi (yang diperkenalkan oleh shell Korn), jadi tidak ada cara untuk menonaktifkan pemrosesan backslash selain pra-pemrosesan input dengan sesuatu seperti di sed 's/\\/&&/g'sana.

Shell Bourne tidak memiliki gagasan tentang dua kelas karakter (yang sekali lagi diperkenalkan oleh ksh). Dalam Bourne shell semua karakter menjalani perlakuan yang sama seperti IFS karakter spasi lakukan di ksh, yaitu IFS=: read a b cpada input seperti foo::barakan menugaskan baruntuk $b, tidak string kosong.

Dalam cangkang Bourne, dengan:

var=value cmd

Jika cmdbuilt-in (seperti readada), vartetap diatur ke valuesetelah cmdselesai. Itu sangat penting dengan $IFSkarena dalam shell Bourne, $IFSdigunakan untuk membagi segalanya, tidak hanya ekspansi. Juga, jika Anda menghapus karakter spasi dari $IFSdalam Bourne shell, "$@"tidak lagi berfungsi.

Di shell Bourne, pengarahan ulang perintah majemuk menyebabkannya berjalan dalam subkulit (dalam versi paling awal, bahkan hal-hal suka read var < fileatau exec 3< file; read var <&3tidak berfungsi), jadi jarang di shell Bourne digunakan readuntuk apa pun selain input pengguna pada terminal (di mana penanganan kelanjutan garis itu masuk akal)

Beberapa Unices (seperti HP / UX, juga ada satu di dalamnya util-linux) masih memiliki lineperintah untuk membaca satu baris input (yang dulunya adalah perintah UNIX standar hingga Spesifikasi Single UNIX versi 2 ).

Itu pada dasarnya sama dengan head -n 1kecuali bahwa itu membaca satu byte pada suatu waktu untuk memastikan itu tidak membaca lebih dari satu baris. Pada sistem itu, Anda dapat melakukan:

line=`line`

Tentu saja, itu berarti memunculkan proses baru, menjalankan perintah dan membaca hasilnya melalui pipa, jadi jauh lebih efisien daripada ksh IFS= read -r line, tetapi masih jauh lebih intuitif.

Stéphane Chazelas
sumber
3
+1 Terima kasih atas wawasan yang bermanfaat tentang perawatan yang berbeda pada ruang / tab vs "lainnya" di IFS di bash ... Saya tahu mereka diperlakukan secara berbeda, tetapi penjelasan ini menyederhanakan semuanya. (Dan wawasan antara bash (dan kerang posix lainnya) dan shperbedaan reguler juga berguna untuk menulis skrip portabel!)
Olivier Dulac
Setidaknya untuk bash-4.4.19, while read -r; do echo "'$REPLY'"; doneberfungsi sebagai while IFS= read -r line; do echo "'$line'"; done.
x-yuri
Ini: "... anggapan keliru bahwa membaca adalah perintah untuk membaca sebuah baris ..." membuat saya berpikir, bahwa jika menggunakan readmembaca suatu garis adalah salah, pasti ada sesuatu yang lain. Apa gagasan yang tidak salah itu? Atau apakah pernyataan pertama itu benar secara teknis, tetapi sebenarnya gagasan yang tidak salah adalah: "membaca adalah perintah untuk membaca kata-kata dari sebuah baris. Karena begitu kuat, Anda dapat menggunakannya untuk membaca baris dari file dengan melakukan: IFS= read -r line"
Mike S
8

Teori

Ada dua konsep yang berperan di sini:

  • IFSadalah Pemisah Bidang Input, yang berarti pembacaan string akan dibagi berdasarkan karakter dalam IFS. Pada baris perintah, IFSbiasanya karakter spasi apa saja, itu sebabnya baris perintah terbagi spasi.
  • Melakukan sesuatu seperti VAR=value commandberarti "memodifikasi lingkungan perintah sehingga VARakan memiliki nilai value". Pada dasarnya, perintah commandakan melihat VARmemiliki nilai value, tetapi setiap perintah yang dieksekusi setelah itu masih akan melihat VARmemiliki nilai sebelumnya. Dengan kata lain, variabel itu hanya akan dimodifikasi untuk pernyataan itu.

Pada kasus ini

Jadi ketika melakukan IFS= read -r line, apa yang Anda lakukan adalah mengatur IFSke string kosong (tidak ada karakter yang akan digunakan untuk membelah, oleh karena itu tidak akan terjadi pemisahan) sehingga readakan membaca seluruh baris dan melihatnya sebagai satu kata yang akan ditugaskan ke linevariabel. Perubahan IFShanya mempengaruhi pernyataan itu, sehingga perintah berikut tidak akan terpengaruh oleh perubahan.

Sebagai catatan

Sementara perintah adalah benar dan akan bekerja sebagaimana dimaksud, pengaturan IFSdalam hal ini tidak sekuat 1 tidak diperlukan. Seperti yang tertulis di bashhalaman manual di bagian readbuiltin:

Satu baris dibaca dari input standar [...] dan kata pertama ditetapkan ke nama depan, kata kedua ke nama kedua, dan seterusnya, dengan kata-kata sisa dan pemisah interveningnya ditetapkan ke nama belakang . Jika ada lebih sedikit kata yang dibaca dari aliran input dari nama, nama yang tersisa diberikan nilai kosong. Karakter dalam IFSdigunakan untuk membagi garis menjadi kata-kata. [...]

Karena Anda hanya memiliki linevariabel, toh setiap kata akan ditugaskan untuk itu, jadi jika Anda tidak memerlukan karakter spasi putih sebelumnya dan trailing 1, Anda bisa menulis read -r linedan selesai melakukannya.

[1] Sama seperti contoh bagaimana suatu nilai unsetdefault $IFSakan menyebabkan readmenganggap / membuntuti spasi IFS , Anda dapat mencoba:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Jalankan dan Anda akan melihat bahwa karakter sebelumnya dan trailing tidak akan bertahan jika IFStidak disetel. Selain itu, beberapa hal aneh bisa terjadi jika $IFSharus dimodifikasi di suatu tempat sebelumnya dalam skrip.

pengguna43791
sumber
5

Anda harus membaca pernyataan itu dalam dua bagian, yang pertama membersihkan nilai variabel IFS, yaitu setara dengan yang lebih mudah dibaca IFS="", yang kedua membaca linevariabel dari stdin read -r line,.

Apa yang spesifik dalam sintaks ini adalah pengaruh IFS yang transcient dan hanya valid untuk readperintah.

Kecuali jika saya melewatkan sesuatu, dalam hal itu kliring IFStidak memiliki efek apa pun karena apa pun IFSyang diatur, seluruh baris akan dibaca dalam linevariabel. Akan ada perubahan perilaku hanya dalam kasus ini lebih dari satu variabel telah dilewati sebagai parameter untuk readinstruksi.

Sunting:

Itu -rada untuk memungkinkan input berakhir dengan \tidak akan diproses secara khusus, yaitu untuk backslash untuk dimasukkan dalam linevariabel dan bukan sebagai karakter lanjutan untuk memungkinkan input multi-line.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

Menghapus IFS memiliki efek samping mencegah pembacaan untuk memangkas karakter tab atau spasi potensial dan tertinggal, misalnya:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Terima kasih kepada Rici untuk menunjukkan perbedaan itu.

Jlliagre
sumber
Apa yang Anda lewatkan adalah bahwa jika IFS tidak diubah, read -r lineakan memangkas spasi awal dan akhir sebelum menetapkan input ke linevariabel.
rici
@rici Saya mencurigai sesuatu seperti itu tetapi hanya memeriksa karakter IFS di antara kata-kata, bukan yang memimpin / mengekor. Terima kasih telah menunjukkan fakta itu!
jlliagre
membersihkan IFS juga akan mencegah penugasan beberapa variabel (efek samping). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"akan ditampilkan-aa bb--
kyodev