ทำความเข้าใจกับ IFS


71

หัวข้อต่อไปนี้บนไซต์นี้และ StackOverflow มีประโยชน์สำหรับการทำความเข้าใจวิธีการIFSทำงาน:

แต่ฉันยังมีคำถามสั้น ๆ ฉันตัดสินใจถามพวกเขาในโพสต์เดียวกันเนื่องจากฉันคิดว่ามันอาจช่วยผู้อ่านในอนาคตได้ดีขึ้น:

ไตรมาสที่ 1 IFSโดยทั่วไปจะกล่าวถึงในบริบทของ "การแยกฟิลด์" การแบ่งฟิลด์เหมือนกับการแยกคำหรือไม่

Q2:ข้อกำหนด POSIX บอกว่า :

หากค่าของ IFS เป็นโมฆะจะไม่มีการแยกฟิลด์

การตั้งค่าIFS=เหมือนกับการตั้งค่าIFSเป็นโมฆะหรือไม่ นี่คือสิ่งที่มีความหมายโดยการตั้งค่าempty stringด้วยหรือไม่

Q3:ในข้อมูลจำเพาะ POSIX ฉันอ่านต่อไปนี้:

หากไม่ได้ตั้งค่า IFS เชลล์จะทำงานเสมือนว่าค่าของ IFS นั้นคือ <space>, <tab> and <newline>

IFSบอกว่าผมต้องการที่จะคืนค่าเริ่มต้นของ ฉันจะทำอย่างไร (โดยเฉพาะอย่างยิ่งฉันจะอ้างถึง<tab>และ<newline>อย่างไร)

Q4:สุดท้ายรหัสนี้จะเป็นอย่างไร:

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

ประพฤติถ้าเราเปลี่ยนบรรทัดแรกเป็น

while read -r line # Use the default IFS value

หรือถึง:

while IFS=' ' read -r line

คำตอบ:


28
  1. ใช่พวกเขาเหมือนกัน
  2. ใช่.
  3. ใน bash และ shell ที่คล้ายกันคุณสามารถทำอะไรIFS=$' \t\n'ได้บ้าง [space] CTRL+V [tab] CTRL+V [enter]มิฉะนั้นคุณอาจใส่รหัสควบคุมตัวอักษรโดยใช้ หากคุณวางแผนที่จะทำเช่นนี้จะเป็นการดีกว่าถ้าใช้ตัวแปรอื่นเพื่อเก็บIFSค่าเก่าชั่วคราวจากนั้นเรียกคืนหลังจากนั้น (หรือแทนที่ทับซ้อนชั่วคราวสำหรับคำสั่งเดียวโดยใช้var=foo commandไวยากรณ์)
    • ข้อมูลโค้ดแรกจะทำให้การอ่านทั้งบรรทัดเป็นคำต่อคำ$lineเนื่องจากไม่มีตัวคั่นฟิลด์ที่จะทำการแยกคำให้ อย่างไรก็ตามโปรดจำไว้ว่าเนื่องจากกระสุนจำนวนมากใช้ cstrings เพื่อจัดเก็บสตริงตัวอย่างแรกของ NUL อาจยังทำให้รูปลักษณ์ของมันถูกยกเลิกก่อนกำหนด
    • ข้อมูลโค้ดที่สองอาจไม่ได้คัดลอกอินพุตที่$lineแน่นอน ตัวอย่างเช่นหากมีตัวคั่นฟิลด์ต่อเนื่องหลายรายการจะถูกสร้างเป็นอินสแตนซ์เดียวขององค์ประกอบแรก สิ่งนี้มักได้รับการยอมรับว่าเป็นการสูญเสียพื้นที่โดยรอบ
    • ข้อมูลโค้ดที่สามจะทำเหมือนกับรหัสที่สองยกเว้นจะแยกเฉพาะในช่องว่าง (ไม่ใช่ช่องว่างแท็บหรือบรรทัดใหม่ตามปกติ)

3
คำตอบสำหรับ Q2 นั้นผิด: การที่ว่างเปล่าIFSและ unset IFSแตกต่างกันมาก คำตอบสำหรับไตรมาสที่ 4 นั้นผิดพลาดบางส่วน: ตัวคั่นภายในไม่ได้ถูกแตะต้องที่นี่เพียงนำหน้าและต่อท้ายเท่านั้น
Gilles

3
@Gilles: ในไตรมาสที่ 2 ไม่มีของทั้งสามได้รับนิกายหมายถึงไม่มีการตั้งค่าทั้งหมดของพวกเขาหมายถึงIFS IFS=
Stéphane Gimenez

@Gilles ในไตรมาสที่ 2 ฉันไม่เคยพูดว่าพวกเขาเหมือนกัน และตัวคั่นภายในถูกสัมผัสดังที่แสดงไว้ที่นี่: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (เอ่ออะไรควรมีตัวคั่นหลายช่องว่างในนั้นดังนั้นเครื่องยนต์จะทำการลอกออกมา)
Chris Down

2
@ StéphaneGimenez, Chris: ใช่แล้วขอโทษด้วยเกี่ยวกับ Q2 ฉันอ่านคำถามผิด สำหรับไตรมาสที่ 4 เรากำลังพูดถึงread; ตัวแปรสุดท้ายจะคว้าทุกสิ่งที่เหลือยกเว้นตัวคั่นสุดท้ายและออกจากตัวคั่นภายใน
Gilles

1
Gilles นั้นถูกต้องบางส่วนเกี่ยวกับช่องว่างที่ไม่ถูกลบโดยการอ่าน อ่านคำตอบของฉันสำหรับรายละเอียด

22

Q1: ใช่ “ การแยกฟิลด์” และ“ การแยกคำ” เป็นคำสองคำสำหรับแนวคิดเดียวกัน

Q2: ใช่ หากIFSไม่ได้ตั้งค่า (เช่นหลังจากนั้นunset IFS) จะเท่ากับIFSการตั้งค่าเป็น$' \t\n'(ช่องว่างแท็บและขึ้นบรรทัดใหม่) หากIFSตั้งค่าเป็นค่าว่าง (นั่นคือ“ null” หมายถึงที่นี่) (เช่นหลังจากIFS=หรือIFS=''หรือIFS="") จะไม่มีการแยกฟิลด์ใด ๆ เลย (และ$*โดยปกติจะใช้อักขระตัวแรกของ$IFSใช้อักขระเว้นวรรค)

ไตรมาสที่ 3 ถ้าคุณต้องการที่จะมีการเริ่มต้นพฤติกรรมที่คุณสามารถใช้IFS unset IFSหากคุณต้องการตั้งค่าIFSเริ่มต้นนี้อย่างชัดเจนคุณสามารถกำหนดพื้นที่ตัวอักษรแท็บบรรทัดใหม่ในเครื่องหมายคำพูดเดี่ยว ใน ksh93, bash หรือ zsh, คุณสามารถIFS=$' \t\n'ใช้ได้ ถ้าคุณต้องการหลีกเลี่ยงการมีแท็บตัวอักษรในไฟล์ต้นฉบับคุณสามารถใช้

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

Q4: เมื่อIFSตั้งค่าเป็นค่าว่างให้read -r lineตั้งค่าlineเป็นทั้งบรรทัดยกเว้นการขึ้นบรรทัดใหม่ ด้วยIFS=" "ช่องว่างที่จุดเริ่มต้นและจุดสิ้นสุดของบรรทัดถูกตัดแต่ง ด้วยค่าเริ่มต้นของIFSแท็บและช่องว่างจะถูกตัดแต่ง


2
Q2 เป็นส่วนหนึ่งที่ผิด หาก IFS ว่างเปล่า "$ *" จะถูกรวมโดยไม่มีตัวคั่น (สำหรับ$@มีบางรูปแบบระหว่างเชลล์ในบริบทที่ไม่ใช่รายการเช่นIFS=; var=$@) ควรสังเกตว่าเมื่อ IFS ว่างเปล่าไม่มีการแบ่งคำใด ๆ ที่สมบูรณ์แบบ แต่ $ var ยังคงขยายตัวเป็นอาร์กิวเมนต์ไม่แทนอาร์กิวเมนต์ว่างเมื่อ $ var ว่างเปล่าและ globbing ยังคงใช้ดังนั้นคุณยังต้องพูดตัวแปร (แม้ว่าคุณจะ ปิดการใช้งาน globbing)
Stéphane Chazelas

13

ไตรมาสที่ 1 การแยกฟิลด์

การแบ่งฟิลด์เหมือนกับการแยกคำหรือไม่

ใช่ทั้งคู่ชี้ไปที่แนวคิดเดียวกัน

Q2: IFSเป็นโมฆะเมื่อใด

การตั้งค่าIFS=''เป็นค่าว่างเช่นเดียวกับสตริงว่างเปล่าด้วยหรือไม่

ใช่ทั้งสามหมายถึงเหมือนกัน: จะไม่มีการแยกฟิลด์ / คำ นอกจากนี้ยังส่งผลต่อฟิลด์การพิมพ์ (เช่นเดียวกับecho "$*") ฟิลด์ทั้งหมดจะถูกต่อกันพร้อมกันโดยไม่มีที่ว่าง

Q3: (ส่วนหนึ่ง) Unset IFS

ในข้อมูลจำเพาะ POSIX ฉันอ่านต่อไปนี้ :

ถ้าไอเอฟเอไม่ได้ตั้งเปลือกจะประพฤติตัวเป็นถ้าค่าของไอเอฟเอเป็น<พื้นที่> <แท็บ> <newline>

ซึ่งเทียบเท่ากับ:

ด้วยunset IFSเปลือกจะทำตัวราวกับว่า IFS เป็นค่าเริ่มต้น

นั่นหมายความว่า 'การแยกฟิลด์' จะเหมือนกันทุกประการด้วยค่า IFS เริ่มต้นหรือไม่ได้ตั้งค่า
นั่นไม่ได้หมายความว่า IFS จะทำงานในลักษณะเดียวกันในทุกสภาวะ การเจาะจงมากขึ้นการเรียกใช้งานOldIFS=$IFSจะตั้งค่า var OldIFSเป็นnullไม่ใช่ค่าเริ่มต้น และพยายามตั้งค่า IFS กลับเช่นนี้IFS=OldIFSจะตั้งค่า IFS เป็น null ไม่เก็บไว้เหมือนเมื่อก่อน ระวัง !!.

Q3: (ส่วน b) กู้คืน IFS

ฉันจะคืนค่า IFS เป็นค่าเริ่มต้นได้อย่างไร ว่าฉันต้องการคืนค่าเริ่มต้นของ IFS ฉันจะทำอย่างไร (โดยเฉพาะอย่างยิ่งฉันจะอ้างถึง<tab>และ<newline> ได้อย่างไร)

สำหรับ zsh, ksh และ bash (AFAIK) สามารถตั้งค่า IFS เป็นค่าเริ่มต้นเป็น:

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

เสร็จแล้วคุณต้องอ่านอะไรอีก

แต่ถ้าคุณต้องการตั้งค่า IFS ใหม่สำหรับ sh มันอาจซับซ้อน

ลองมาดูจากง่ายที่สุดเพื่อให้เสร็จสมบูรณ์โดยไม่มีข้อบกพร่อง (ยกเว้นความซับซ้อน)

1.- ยกเลิก IFS

เราทำได้unset IFS(อ่านไตรมาสที่ 3 ก, เหนือ)

2.- แลกเปลี่ยนตัวอักษร

การแก้ปัญหาการสลับค่าของแท็บและการขึ้นบรรทัดใหม่ทำให้การตั้งค่าของ IFS ง่ายขึ้นจากนั้นจะทำงานในลักษณะที่เท่าเทียมกัน

ตั้งค่า IFS เป็น<space><newline> <tab> :

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

3.- ง่าย? วิธีการแก้:

หากมีสคริปต์ลูกที่ต้องตั้งค่า IFS อย่างถูกต้องคุณสามารถเขียนด้วยตนเองได้เสมอ:

ไอเอฟเอ ='   
'

โดยที่ลำดับที่พิมพ์ด้วยตนเองคือ: IFS='spacetabnewline'ลำดับที่พิมพ์จริงอย่างถูกต้องด้านบน (หากคุณต้องการยืนยันให้แก้ไขคำตอบนี้) แต่การคัดลอก / วางจากเบราว์เซอร์ของคุณจะแตกเพราะเบราว์เซอร์จะบีบ / ซ่อนช่องว่าง มันทำให้ยากที่จะแบ่งปันรหัสตามที่เขียนไว้ข้างต้น

4.- โซลูชั่นที่สมบูรณ์

ในการเขียนโค้ดที่สามารถคัดลอกได้อย่างปลอดภัยมักจะเกี่ยวข้องกับการหลีกเลี่ยงการพิมพ์ที่ชัดเจน

เราต้องการรหัสที่ "สร้าง" ค่าที่คาดหวัง แต่แม้ว่าจะถูกต้องในเชิงแนวคิดรหัสนี้จะไม่ตั้งค่าต่อท้าย\n:

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

ที่เกิดขึ้นเนื่องจากภายใต้เชลล์ส่วนใหญ่บรรทัดใหม่ต่อท้าย$(...)หรือการ`...`แทนที่คำสั่งทั้งหมดจะถูกลบออกเมื่อมีการขยาย

เราจำเป็นต้องใช้เคล็ดลับสำหรับการดวลจุดโทษ:

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

ทางเลือกอื่นอาจเป็นการตั้งค่า IFS เป็นค่าสภาพแวดล้อมจาก bash (ตัวอย่าง) และจากนั้นเรียก sh (รุ่นที่ยอมรับ IFS ที่จะตั้งค่าผ่านสภาพแวดล้อม) ดังนี้:

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

ในระยะสั้น sh ทำให้การรีเซ็ต IFS เป็นค่าเริ่มต้นค่อนข้างเป็นการผจญภัยที่แปลก

Q4: ในรหัสจริง:

สุดท้ายรหัสนี้จะเป็นอย่างไร:

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

ประพฤติถ้าเราเปลี่ยนบรรทัดแรกเป็น

while read -r line # Use the default IFS value

หรือถึง:

while IFS=' ' read -r line

ครั้งแรก: ฉันไม่ทราบว่าecho $line(มี var ไม่ได้อ้างอิง) มีอยู่ใน porpouse หรือไม่ มันแนะนำระดับที่สองของ 'การแยกฟิลด์' ที่อ่านไม่ได้ ดังนั้นฉันจะตอบทั้งคู่ :)

ด้วยรหัสนี้ (เพื่อให้คุณสามารถยืนยันได้) คุณจะต้องมีประโยชน์ xxd :

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

ฉันเข้าใจ:

$ ./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

ค่าแรกเป็นเพียงค่าที่ถูกต้องของ IFS='spacetabnewline'

บรรทัดถัดไปคือค่า$aเลขฐานสิบหกทั้งหมดที่ var มีและขึ้นบรรทัดใหม่ '0a' ที่ส่วนท้ายซึ่งจะถูกกำหนดให้กับแต่ละคำสั่ง read

บรรทัดถัดไปซึ่ง IFS เป็นโมฆะจะไม่ทำการ 'แยกฟิลด์' แต่จะขึ้นบรรทัดใหม่ (ตามที่คาดไว้)

สามบรรทัดถัดไปเนื่องจาก IFS มีช่องว่างให้ลบช่องว่างเริ่มต้นและตั้งค่าบรรทัด var เป็นยอดเงินคงเหลือ

สี่บรรทัดสุดท้ายแสดงให้เห็นว่าตัวแปรที่ไม่มีเครื่องหมายจะทำอะไร ค่าจะถูกแบ่งในพื้นที่ (หลาย) และจะถูกพิมพ์เป็น:bar,baz,qux,


4

unset IFS ไม่ล้าง IFS แม้ว่า IFS จะสันนิษฐานว่าเป็น "\ t \ n" หลังจากนั้น:

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

ทดสอบกับ bash เวอร์ชั่น 4.2.45 และ 3.2.25 ที่มีพฤติกรรมเหมือนกัน


คำถามและเอกสารที่เชื่อมโยงไม่ได้พูดคุยเกี่ยวกับunsetการIFSตามที่อธิบายไว้ในความคิดเห็นของคำตอบที่ยอมรับได้ที่นี่
ILMostro_7
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.