Memahami IFS

71

Beberapa utas berikut di situs ini dan StackOverflow sangat membantu untuk memahami cara IFSkerjanya:

Tetapi saya masih memiliki beberapa pertanyaan pendek. Saya memutuskan untuk bertanya kepada mereka di pos yang sama karena saya pikir itu dapat membantu pembaca masa depan yang lebih baik:

Q1. IFSbiasanya dibahas dalam konteks "field splitting". Apakah pemisahan bidang sama dengan pemisahan kata ?

Q2: Spesifikasi POSIX mengatakan :

Jika nilai IFS adalah nol, tidak ada pemisahan bidang yang harus dilakukan.

Apakah pengaturan IFS=sama dengan pengaturan IFSke nol? Apakah ini yang dimaksud dengan mengaturnya empty stringjuga?

Q3: Dalam spesifikasi POSIX, saya membaca yang berikut:

Jika IFS tidak disetel, shell akan berperilaku seolah-olah nilai IFS adalah <space>, <tab> and <newline>

Katakanlah saya ingin mengembalikan nilai default IFS. Bagaimana aku melakukan itu? (lebih khusus, bagaimana saya merujuk <tab>dan <newline>?)

T4: Akhirnya, bagaimana kode ini:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

berperilaku jika kita mengubah baris pertama ke

while read -r line # Use the default IFS value

atau untuk:

while IFS=' ' read -r line
Amelio Vazquez-Reina
sumber

Jawaban:

28
  1. Ya, mereka sama.
  2. Iya.
  3. Dalam bash, dan shell serupa, Anda dapat melakukan sesuatu seperti IFS=$' \t\n'. Jika tidak, Anda bisa memasukkan kode kontrol literal dengan menggunakan [space] CTRL+V [tab] CTRL+V [enter]. Namun, jika Anda berencana untuk melakukan ini, lebih baik menggunakan variabel lain untuk sementara menyimpan nilai lama IFS, dan kemudian mengembalikannya setelah itu (atau menimpanya sementara untuk satu perintah dengan menggunakan var=foo commandsintaks).
    • Cuplikan kode pertama akan membuat seluruh baris terbaca, kata demi kata, menjadi $line, karena tidak ada pemisah bidang untuk melakukan pemisahan kata. Namun perlu diingat bahwa karena banyak cangkang menggunakan cstring untuk menyimpan dawai, contoh pertama dari NUL masih dapat menyebabkan penampilan itu dihentikan sebelum waktunya.
    • Cuplikan kode kedua mungkin tidak memasukkan salinan input yang tepat ke dalamnya $line. Misalnya, jika ada beberapa pemisah bidang berurutan, mereka akan dibuat menjadi sebuah instance tunggal dari elemen pertama. Ini sering diakui sebagai hilangnya ruang putih di sekitarnya.
    • Cuplikan kode ketiga akan melakukan hal yang sama seperti yang kedua, kecuali hanya akan terpecah pada spasi (bukan spasi, tab, atau baris baru).
Chris Down
sumber
3
Jawaban untuk Q2 salah: yang kosong IFSdan tidak disetel IFSsangat berbeda. Jawaban untuk Q4 sebagian salah: pemisah bagian dalam tidak disentuh di sini, hanya yang mengarah dan yang tertinggal.
Gilles 'SO- berhenti menjadi jahat'
3
@Gilles: Di Q2, tidak satu pun dari tiga denominasi yang diberikan merujuk pada yang tidak disetel IFS, semuanya berarti IFS=.
Stéphane Gimenez
@Gilles Di Q2, saya tidak pernah mengatakan mereka sama. Dan pemisah dalam disentuh, seperti ditunjukkan di sini: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Er, apa? Harus ada beberapa pembatas ruang di sana, mesin SO terus melucuti mereka).
Chris Down
2
@ StéphaneGimenez, Chris: Oh, benar, maaf tentang Q2, saya salah membaca pertanyaan. Untuk Q4, kita berbicara tentang read; variabel terakhir mengambil semua yang tersisa kecuali untuk pemisah terakhir dan meninggalkan pemisah dalam.
Gilles 'SANGAT berhenti menjadi jahat'
1
Gilles sebagian benar tentang ruang yang tidak dihapus oleh baca. Baca jawaban saya untuk detailnya.
22

Q1: Ya. "Field splitting" dan "word splitting" adalah dua istilah untuk konsep yang sama.

T2: Ya. Jika IFStidak disetel (yaitu setelah unset IFS), itu sama IFSdengan disetel ke $' \t\n'(spasi, tab, dan baris baru). Jika IFSdiatur ke nilai kosong (itulah yang artinya "null" di sini) (yaitu setelah IFS=atau IFS=''atau IFS=""), tidak ada pemisahan bidang yang dilakukan sama sekali (dan $*, yang biasanya menggunakan karakter pertama $IFS, menggunakan karakter spasi).

T3: Jika Anda ingin memiliki IFSperilaku default , Anda dapat menggunakan unset IFS. Jika Anda ingin menetapkan IFSsecara eksplisit ke nilai default ini, Anda dapat menempatkan spasi karakter spasi, tab, baris baru dalam tanda kutip tunggal. Di ksh93, bash atau zsh, Anda bisa menggunakan IFS=$' \t\n'. Mudah-mudahan, jika Anda ingin menghindari memiliki karakter tab literal dalam file sumber Anda, Anda dapat menggunakannya

IFS=" $(echo t | tr t \\t)
"

Q4: Dengan IFSset ke nilai kosong, read -r linesetel lineke seluruh baris kecuali baris yang mengakhiri. Dengan IFS=" ", spasi di awal dan di ujung garis dipangkas. Dengan nilai default IFS, tab dan spasi dipangkas.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
2
Q2 sebagian salah. Jika IFS kosong, "$ *" bergabung tanpa pemisah. (untuk $@, ada beberapa variasi antara shell dalam konteks non-daftar seperti IFS=; var=$@). Perlu dicatat bahwa ketika IFS kosong, tidak ada pemisahan kata dilakukan tetapi $ var masih memperluas tidak ada argumen, bukan argumen kosong ketika $ var kosong, dan globbing masih berlaku, jadi Anda masih perlu mengutip variabel (bahkan jika Anda disable globbing)
Stéphane Chazelas
13

Q1. Pemisahan bidang.

Apakah pemisahan bidang sama dengan pemisahan kata?

Ya, keduanya menunjuk ke ide yang sama.

T2: Kapan IFS nol ?

Apakah pengaturannya IFS=''sama dengan null, sama seperti string kosong juga?

Ya, ketiganya berarti sama: Tidak ada pemisahan bidang / kata yang harus dilakukan. Juga, ini memengaruhi bidang pencetakan (seperti halnya echo "$*") semua bidang akan digabungkan bersama tanpa ruang.

T3: (bagian a) Batalkan IFS.

Dalam spesifikasi POSIX, saya membaca yang berikut :

Jika IFS tidak disetel, shell akan berperilaku seolah-olah nilai IFS adalah <spasi> <tab> <newline> .

Yang persis sama dengan:

Dengan unset IFS, shell akan berperilaku seolah-olah IFS adalah default.

Itu berarti 'Field splitting' akan persis sama dengan nilai IFS default, atau tidak disetel.
Itu TIDAK berarti bahwa IFS akan bekerja dengan cara yang sama di semua kondisi. Menjadi lebih spesifik, mengeksekusi OldIFS=$IFSakan mengatur var OldIFSmenjadi nol , bukan default. Dan mencoba untuk mengatur IFS kembali, karena ini, IFS=OldIFSakan mengatur IFS ke nol, tidak membiarkannya tetap seperti sebelumnya. Awas !!.

T3: (bagian b) Kembalikan IFS.

Bagaimana saya bisa mengembalikan nilai IFS ke default. Katakanlah saya ingin mengembalikan nilai default IFS. Bagaimana aku melakukan itu? (lebih khusus, bagaimana cara merujuk ke <tab> dan <newline> ?)

Untuk zsh, ksh, dan bash (AFAIK), IFS dapat diatur ke nilai default sebagai:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Selesai, Anda tidak perlu membaca yang lain.

Tetapi jika Anda perlu mengatur ulang IFS untuk sh, itu mungkin menjadi kompleks.

Mari kita lihat dari yang termudah hingga selesai tanpa kekurangan (kecuali kompleksitas).

1.- Batalkan IFS.

Kita bisa saja unset IFS(Baca bagian Q3 a, di atas.).

2.- Tukar karakter.

Sebagai solusinya, menukar nilai tab dan baris baru membuatnya lebih mudah untuk mengatur nilai IFS, dan kemudian bekerja dengan cara yang setara.

Setel IFS ke <spasi><newline> <tab> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- Sederhana? larutan:

Jika ada skrip anak yang memerlukan IFS diatur dengan benar, Anda selalu bisa menulis secara manual:

IFS = '   
'

Di mana urutan yang diketik secara manual adalah:, IFS='spacetabnewline'urutan yang sebenarnya telah diketik dengan benar di atas (Jika Anda perlu mengonfirmasi, edit jawaban ini). Tetapi copy / paste dari browser Anda akan rusak karena browser akan menekan / menyembunyikan spasi. Itu membuatnya sulit untuk membagikan kode seperti yang ditulis di atas.

4.- Solusi lengkap.

Untuk menulis kode yang dapat disalin dengan aman biasanya melibatkan jalan keluar yang jelas.

Kami membutuhkan beberapa kode yang "menghasilkan" nilai yang diharapkan. Tetapi, meskipun secara konsepsi benar, kode ini TIDAK akan menetapkan trailing \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

Itu terjadi karena, di sebagian besar shell, semua baris baru $(...)atau `...`pergantian perintah dihapus pada ekspansi.

Kita perlu menggunakan trik untuk sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Cara alternatif mungkin untuk menetapkan IFS sebagai nilai lingkungan dari bash (misalnya) dan kemudian memanggil sh (versi yang menerima IFS untuk diatur melalui lingkungan), karena ini:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

Singkatnya, sh membuat mengatur ulang IFS ke default cukup petualangan yang aneh.

Q4: Dalam kode aktual:

Akhirnya, bagaimana kode ini:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

berperilaku jika kita mengubah baris pertama ke

while read -r line # Use the default IFS value

atau untuk:

while IFS=' ' read -r line

Pertama: Saya tidak tahu apakah echo $line(dengan var TIDAK dikutip) ada di porpouse, atau tidak. Ini memperkenalkan level kedua 'field splitting' yang tidak dimiliki read. Jadi saya akan menjawab keduanya. :)

Dengan kode ini (jadi Anda bisa mengonfirmasi). Anda membutuhkan xxd yang berguna :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Saya mendapat:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

Nilai pertama adalah nilai yang benar IFS='spacetabnewline'

Baris berikutnya adalah semua nilai hex yang dimiliki var $a, dan baris baru '0a' di akhir karena akan diberikan untuk setiap perintah baca.

Baris berikutnya, yang IFS-nya nol, tidak melakukan 'pemisahan bidang', tetapi baris baru dihapus (seperti yang diharapkan).

Tiga baris berikutnya, karena IFS berisi spasi, hapus spasi awal dan atur garis var ke sisa saldo.

Empat baris terakhir menunjukkan apa yang akan dilakukan oleh variabel yang tidak dikutip. Nilai akan dibagi pada (beberapa) spasi dan akan dicetak sebagai:bar,baz,qux,


sumber
4

unset IFS tidak menghapus IFS, bahkan jika IFS setelah itu dianggap "\ t \ n":

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Diuji pada versi bash 4.2.45 dan 3.2.25 dengan perilaku yang sama.

derekm
sumber
Pertanyaan dan dokumentasi terkait tidak berbicara tentang unsetdari IFS, seperti yang dijelaskan dalam komentar-komentar dari jawaban yang diterima di sini.
ILMostro_7