ทำความเข้าใจกับ“ IFS = read -r line”


60

ฉันเข้าใจชัดเจนว่าสามารถเพิ่มค่าให้กับตัวแปรตัวคั่นฟิลด์ภายในได้ ตัวอย่างเช่น:

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

ฉันยังเข้าใจว่าread -r lineจะบันทึกข้อมูลจากstdinไปยังชื่อตัวแปรline:

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

อย่างไรก็ตามคำสั่งสามารถกำหนดค่าตัวแปรได้อย่างไร และเก็บข้อมูลจากstdinตัวแปรแรกlineแล้วให้ค่าของlineถึงIFSหรือไม่?


3
ที่เกี่ยวข้อง: unix.stackexchange.com/q/169716/38906
cuonglm

คำตอบ:


104

บางคนมีความคิดที่ผิดพลาดนั่นreadคือคำสั่งให้อ่านบรรทัด มันไม่ใช่.

readอ่านคำจากบรรทัด (อาจเป็นแบ็กสแลช - ต่อ) ซึ่งคำนั้น$IFSคั่นด้วยและแบ็กสแลชสามารถใช้เพื่อหลีกเลี่ยงตัวคั่น (หรือต่อบรรทัด)

ไวยากรณ์ทั่วไปคือ:

read word1 word2... remaining_words

readอ่าน stdin หนึ่งไบต์ในเวลาจนกว่าจะพบตัวละครที่ไม่ใช้ Escape ขึ้นบรรทัดใหม่ (หรือจุดสิ้นสุดของการป้อนข้อมูล) แยกว่าเป็นไปตามกฎระเบียบที่ซับซ้อนและร้านค้าผลมาจากการแยกที่เข้าสู่$word1, ...$word2$remaining_words

ตัวอย่างเช่นการป้อนข้อมูลเช่น:

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

และมีค่าเริ่มต้นของ$IFS, read a b cจะกำหนด:

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

ทีนี้ถ้าผ่านการโต้แย้งเพียงครั้งเดียวนั่นจะไม่เกิดread lineขึ้น read remaining_wordsก็ยังคง การประมวลผลแบ็กสแลชยังคงดำเนินการอยู่ตัวอักขระช่องว่างของ IFS ยังคงถูกลบออกจากจุดเริ่มต้นและจุดสิ้นสุด

-rตัวเลือกเอาการประมวลผลเครื่องหมาย ดังนั้นคำสั่งเดียวกันข้างต้นด้วย-rแทนที่จะกำหนด

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

ตอนนี้สำหรับส่วนที่แยกเป็นสิ่งสำคัญที่จะต้องตระหนักว่ามีสองคลาสของตัวอักษรสำหรับ$IFS: ตัวละครที่ช่องว่างของ IFS (คือช่องว่างและแท็บ (และขึ้นบรรทัดใหม่ถึงแม้ว่าที่นี่จะไม่สำคัญจนกว่าคุณจะใช้ -d) ที่จะอยู่ในค่าเริ่มต้นของ$IFS) และอื่น ๆ การรักษาตัวละครสองคลาสนั้นแตกต่างกัน

ด้วยIFS=:( :ไม่ใช่ตัวละครช่องว่างของ IFS) ข้อมูลที่ต้องการ:foo::bar::จะถูกแบ่งออกเป็น"", "foo"และ"", barและ""(และพิเศษ""กับการใช้งานบางอย่างแม้ว่ามันจะไม่สำคัญยกเว้นread -a) ในขณะที่ถ้าเราแทนที่ที่:มีพื้นที่แยกจะทำลงไปเพียงและfoo barนั่นคือการนำหน้าและส่วนท้ายจะถูกละเว้นและลำดับของสิ่งนั้นจะถูกมองเหมือนหนึ่ง $IFSมีกฎระเบียบเพิ่มเติมเมื่อช่องว่างและไม่ว่างตัวอักษรมีการรวมกันในการเป็น การใช้งานบางอย่างสามารถเพิ่ม / ลบการดูแลเป็นพิเศษได้โดยเพิ่มตัวละครเป็นสองเท่าใน IFS ( IFS=::หรือIFS=' ')

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

ถึงแม้จะมีตัวละครไอเอฟเอที่ไม่ใช่ช่องว่างถ้าสายการป้อนข้อมูลที่มีหนึ่ง (และมีเพียงหนึ่ง) ของตัวละครเหล่านั้นและมันเป็นตัวอักษรตัวสุดท้ายในบรรทัด (เช่นIFS=: read -r wordการป้อนข้อมูลเช่นfoo:) กับ POSIX เปลือกหอย (ไม่ได้zshหรือบางpdkshรุ่น), การป้อนข้อมูลที่ ถือเป็นหนึ่งในfooคำเพราะในเปลือกหอยเหล่านั้นตัวละคร$IFSนี้ถือว่าเป็นจุดสิ้นสุดดังนั้นwordจะมีไม่ได้foofoo:

ดังนั้นวิธีบัญญัติของการอ่านอินพุตหนึ่งบรรทัดด้วยreadบิวอินคือ:

IFS= read -r line

(โปรดทราบว่าสำหรับreadการนำไปใช้ส่วนใหญ่จะใช้งานได้เฉพาะกับบรรทัดข้อความเนื่องจากไม่รองรับอักขระ NUL ยกเว้นในzsh)

การใช้var=value cmdไวยากรณ์ทำให้แน่ใจว่าIFSมีการตั้งค่าแตกต่างกันเฉพาะในช่วงเวลาของcmdคำสั่งนั้น

ประวัติความเป็นมา

readbuiltin ได้รับการแนะนำโดยเปลือกบอร์นและมีอยู่แล้วในการอ่านคำไม่สาย มีความแตกต่างที่สำคัญเล็กน้อยกับเชลล์ POSIX ที่ทันสมัย

บอร์นเชลล์readไม่รองรับ-rตัวเลือก (ซึ่งได้รับการแนะนำโดย Korn เชลล์) ดังนั้นจึงไม่มีวิธีที่จะปิดการใช้งานแบ็กสแลชนอกเหนือจากการประมวลผลอินพุตล่วงหน้าด้วยบางอย่างที่sed 's/\\/&&/g'นั่น

เชลล์เป้าหมายไม่ได้มีความคิดเกี่ยวกับอักขระสองคลาส (ซึ่งแนะนำโดย ksh อีกครั้ง) ใน Bourne shell ตัวละครทุกตัวผ่านการบำบัดเช่นเดียวกับตัวละครที่ช่องว่างของ IFS ทำใน ksh ที่อยู่IFS=: read a b cในการป้อนข้อมูลที่foo::barจะกำหนดbarให้$bไม่ใช่สตริงที่ว่างเปล่า

ในเชลล์เป้าหมายด้วย:

var=value cmd

หากcmdเป็นแบบในตัว (เช่นเดียวกับread) ส่วนที่varเหลือจะถูกตั้งค่าเป็นvalueหลังจากcmdเสร็จสิ้น นั่นเป็นสิ่งสำคัญอย่างยิ่ง$IFSเพราะในเชลล์เป้าหมาย$IFSถูกใช้เพื่อแยกทุกอย่างไม่เพียง แต่การขยาย นอกจากนี้หากคุณลบอักขระช่องว่างออกจาก$IFSในเชลล์เป้าหมาย"$@"ไม่ทำงานอีกต่อไป

ในเชลล์เป้าหมายการเปลี่ยนเส้นทางคำสั่งผสมทำให้มันทำงานใน subshell (ในเวอร์ชันแรกสุดแม้กระทั่งสิ่งที่ชอบread var < fileหรือexec 3< file; read var <&3ไม่ทำงาน) ดังนั้นมันจึงไม่ค่อยเกิดขึ้นใน Bourne shell ที่จะใช้readเพื่ออะไรก็ได้ยกเว้นอินพุตผู้ใช้บนเทอร์มินัล (ในกรณีที่การจัดการความต่อเนื่องของบรรทัดนั้นสมเหตุสมผล)

Unices บางอัน (เช่น HP / UX มีอีกหนึ่งในutil-linux) ยังคงมีlineคำสั่งให้อ่านหนึ่งบรรทัดของอินพุต (ซึ่งเคยเป็นคำสั่ง UNIX มาตรฐานจนถึงรุ่น Single UNIX Specification 2 )

โดยพื้นฐานแล้วจะเหมือนกับhead -n 1อ่านยกเว้นทีละหนึ่งไบต์เพื่อให้แน่ใจว่าจะไม่อ่านมากกว่าหนึ่งบรรทัด ในระบบเหล่านั้นคุณสามารถทำได้:

line=`line`

แน่นอนว่าหมายถึงการวางไข่กระบวนการใหม่รันคำสั่งและอ่านเอาต์พุตผ่านไพพ์ดังนั้นจึงมีประสิทธิภาพน้อยกว่า ksh IFS= read -r lineแต่ก็ยังมีสัญชาตญาณมากขึ้น


3
+1 ขอบคุณสำหรับข้อมูลเชิงลึกที่เป็นประโยชน์เกี่ยวกับการรักษาที่แตกต่างกันในสเปซ / แท็บเทียบกับ "อื่น ๆ " ใน IFS ในทุบตี ... ฉันรู้ว่าพวกเขาได้รับการปฏิบัติแตกต่างกัน แต่คำอธิบายนี้ทำให้ทุกอย่างง่ายขึ้นมาก (และความเข้าใจระหว่างทุบตี (และเปลือกหอย POSIX อื่น ๆ ) และปกติshแตกต่างจะเป็นประโยชน์เกินไปที่จะเขียนสคริปแบบพกพา!)
โอลิเวีย Dulac

อย่างน้อยbash-4.4.19, การทำงานเป็นwhile read -r; do echo "'$REPLY'"; done while IFS= read -r line; do echo "'$line'"; done
x-yuri

สิ่งนี้: "... ความคิดที่ผิดพลาดที่อ่านคือคำสั่งให้อ่านบรรทัด ... " ทำให้ฉันคิดว่าถ้าใช้readการอ่านบรรทัดผิดพลาดต้องมีอย่างอื่น ความคิดที่ไม่ผิดพลาดอาจเป็นอะไร? หรือเป็นคำสั่งแรกที่ถูกต้องทางเทคนิค แต่ในความจริงแล้วแนวคิดที่ไม่ผิดพลาดคือ: "read คือคำสั่งเพื่ออ่านคำจากบรรทัดเนื่องจากมันมีประสิทธิภาพมากคุณจึงสามารถใช้มันเพื่ออ่านบรรทัดจากไฟล์โดยทำ: IFS= read -r line"
Mike S

8

ทฤษฎี

มีแนวคิดสองประการที่เล่นอยู่ที่นี่:

  • IFSIFSเป็นฟิลด์ป้อนข้อมูลการแยกซึ่งหมายความว่าอ่านสตริงจะถูกแบ่งอยู่บนพื้นฐานของตัวละครใน บนบรรทัดคำสั่งIFSปกติอักขระช่องว่างใด ๆ นั่นคือเหตุผลที่บรรทัดคำสั่งแยกที่ช่องว่าง
  • การทำสิ่งที่ชอบVAR=value commandหมายถึง "แก้ไขสภาพแวดล้อมของคำสั่งเพื่อที่VARจะมีค่าvalue" โดยทั่วไปแล้วคำสั่งcommandจะเห็นVARว่ามีค่าvalueแต่คำสั่งใด ๆ ที่ดำเนินการหลังจากนั้นจะยังคงเห็นVARว่ามีค่าก่อนหน้านี้ กล่าวอีกนัยหนึ่งตัวแปรนั้นจะถูกแก้ไขสำหรับคำสั่งนั้นเท่านั้น

ในกรณีนี้

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

ในฐานะที่เป็นบันทึกด้านข้าง

ในขณะที่คำสั่งที่ถูกต้องและจะทำงานตามที่ตั้งใจไว้, การตั้งค่าIFSในกรณีนี้ไม่ อาจจะ1จะไม่จำเป็น ตามที่เขียนไว้ในbashman page ในreadส่วน builtin:

หนึ่งบรรทัดถูกอ่านจากอินพุตมาตรฐาน [ ... ] และคำแรกได้รับมอบหมายให้เป็นชื่อแรกที่สองคำกับชื่อที่สองและอื่น ๆด้วยคำพูดที่เหลือและแยกการแทรกแซงของพวกเขาได้รับมอบหมายให้นามสกุล หากมีคำที่อ่านจากอินพุตสตรีมน้อยกว่าชื่อชื่อที่เหลือจะถูกกำหนดค่าว่าง อักขระIFSที่ใช้ในการแบ่งบรรทัดเป็นคำ [ ... ]

เนื่องจากคุณมีlineตัวแปรเท่านั้นทุกคำจะถูกกำหนดให้อยู่ดีดังนั้นถ้าคุณไม่ต้องการอักขระช่องว่าง1 ก่อนหน้าและต่อท้ายคุณสามารถเขียนread -r lineและทำกับมันได้

[1] เช่นเดียวกับตัวอย่างของวิธีการunsetหรือค่าเริ่มต้นที่$IFSจะทำให้เกิดช่องว่างของ IFS ที่readนำหน้า / ต่อท้ายคุณอาจลอง:

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

เรียกใช้และคุณจะเห็นว่าตัวอักษรก่อนหน้าและตัวต่อท้ายจะไม่รอดหากIFSไม่ได้ตั้งค่า นอกจากนี้สิ่งแปลก ๆ บางอย่างอาจเกิดขึ้นได้หาก$IFSต้องมีการปรับเปลี่ยนบางส่วนในสคริปต์ก่อนหน้านี้


5

คุณควรอ่านคำสั่งที่สองส่วนคนแรกล้างค่าของตัวแปร IFS ที่คือเทียบเท่ากับการอ่านมากขึ้นIFS="", คนที่สองจะอ่านlineตัวแปรจาก read -r linestdin,

สิ่งที่เฉพาะเจาะจงในไวยากรณ์นี้คือการส่งผลกระทบต่อ IFS เป็น transcient และถูกต้องสำหรับreadคำสั่งเท่านั้น

ยกเว้นว่าฉันหายไปบางอย่างในกรณีที่การล้างเฉพาะIFSนั้นไม่มีผลแม้ว่าสิ่งที่IFSตั้งไว้คือทั้งบรรทัดจะอ่านในlineตัวแปร จะมีการเปลี่ยนแปลงลักษณะการทำงานเฉพาะในกรณีที่มีตัวแปรมากกว่าหนึ่งตัวถูกส่งผ่านเป็นพารามิเตอร์ไปยังreadคำสั่ง

แก้ไข:

-rจะมีการอนุญาตให้มีการป้อนข้อมูลที่ลงท้ายด้วย\เพื่อไม่ให้การประมวลผลพิเศษเช่นสำหรับเครื่องหมายที่จะถูกรวมอยู่ในlineตัวแปรและไม่เป็นตัวละครที่ต่อเนื่องจะช่วยให้การป้อนข้อมูลหลายสาย

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

การล้างข้อมูลใน IFS นั้นมีผลข้างเคียงของการป้องกันการอ่านเพื่อตัดส่วนหน้าและส่วนท้ายหรือแท็บอักขระที่อาจเกิดขึ้นเช่น:

$ 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]

ขอบคุณ rici ที่ชี้ให้เห็นความแตกต่างนั้น


สิ่งที่คุณขาดหายไปก็คือถ้าหาก IFS ไม่เปลี่ยนแปลงread -r lineจะตัดขอบช่องว่างด้านหน้าและด้านหลังก่อนกำหนดอินพุตให้กับlineตัวแปร
rici

@rici ฉันสงสัยบางอย่างเช่นนั้น แต่เพียงตรวจสอบอักขระ IFS ระหว่างคำต่าง ๆ ไม่นำหน้า / ต่อท้าย ขอบคุณที่บอกความจริง!
jlliagre

การล้าง IFS จะป้องกันการกำหนดตัวแปรหลายตัว (ผลข้างเคียง) IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"จะแสดง-aa bb--
kyodev
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.