มันเป็นวิธีที่มีเหตุผลในการ“ สำรองข้อมูล” ตัวแปร $ IFS หรือไม่


19

ฉันลังเลที่จะยุ่งกับ$IFSมันอยู่เสมอเพราะมันเป็นสิ่งที่ขัดขวางโลก

แต่บ่อยครั้งมันทำให้การโหลดสตริงลงในอาเรย์ของ bash นั้นดีและกระชับและสำหรับสคริปของ bash นั้นความรัดกุมยากที่จะเกิดขึ้น

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

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


ทำไมถึงไม่ใช้งานได้จริง?
Bratchley

เนื่องจากการยกเลิกการตั้งค่า IFS จะทำงานได้ดี
llua

1
สำหรับผู้ที่บอกว่าไอเอฟเอ unsetting จะทำงานได้ดีเก็บไว้ในใจว่ามันเป็นสถานการณ์: stackoverflow.com/questions/39545837/... จากประสบการณ์ของฉันการตั้งค่า IFS ด้วยตนเองเป็นค่าเริ่มต้นสำหรับตัวแปลเชลล์ของคุณคือ$' \t\n'ถ้าคุณใช้ bash unset $IFSเพียง แต่ไม่คืนค่าเป็นสิ่งที่คุณคาดหวังว่าจะเป็นค่าเริ่มต้น
Darrel Holt

คำตอบ:


9

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

ในฐานะที่เป็น @llua กล่าวถึงในความคิดเห็นของเขากับคำถามของคุณเพียงแค่ยกเลิกการตั้งค่า IFS จะคืนค่าพฤติกรรมเริ่มต้นเทียบเท่ากับการกำหนดพื้นที่แท็บ - ขึ้นบรรทัดใหม่

ควรพิจารณาว่าจะเป็นปัญหาได้อย่างไรหากไม่กำหนด / ยกเลิกการตั้งค่า IFS อย่างชัดเจนมากกว่าที่จะทำ

จาก POSIX 2013 edition, ตัวแปรเชลล์ 2.5.3 :

การนำไปใช้อาจละเว้นค่าของ IFS ในสภาพแวดล้อมหรือไม่มี IFS จากสภาพแวดล้อม ณ เวลาที่เชลล์ถูกเรียกใช้ในกรณีนี้เชลล์จะตั้งค่า IFS เป็น <space> <tab> <newline> เมื่อมีการเรียกใช้ .

เชลล์ที่ถูกเรียก POSIX ซึ่งเป็นไปตาม POSIX อาจมีหรือไม่มีสืบทอด IFS จากสภาพแวดล้อมของมัน จากนี้ต่อไปนี้:

  • สคริปต์แบบพกพาไม่สามารถพึ่งพา IFS ที่สืบทอดผ่านทางสภาพแวดล้อมได้
  • สคริปต์ที่ตั้งใจจะใช้เฉพาะพฤติกรรมการแยกเริ่มต้น (หรือการเข้าร่วมในกรณีของ"$*") แต่อาจทำงานภายใต้เชลล์ที่เริ่มต้น IFS จากสภาพแวดล้อมต้องตั้งค่า / ยกเลิกการตั้งค่า IFS อย่างชัดเจนเพื่อป้องกันตัวเองจากการบุกรุกทางสิ่งแวดล้อม

NB เป็นสิ่งสำคัญที่ต้องเข้าใจว่าสำหรับการสนทนานี้คำว่า "เรียกใช้" มีความหมายเฉพาะ เชลล์จะถูกเรียกใช้เฉพาะเมื่อมันถูกเรียกอย่างชัดเจนโดยใช้ชื่อของมัน (รวมถึง#!/path/to/shellshebang) เชลล์ย่อย - เช่นอาจถูกสร้างขึ้นโดย$(...)หรือcmd1 || cmd2 &- ไม่ใช่เชลล์ที่ถูกเรียกใช้และ IFS (พร้อมกับสภาพแวดล้อมการประมวลผลส่วนใหญ่) นั้นเหมือนกับของพาเรนต์ เชลล์ที่เรียกใช้ตั้งค่าของ$pid ในขณะที่ subshells สืบทอด


นี่ไม่ได้เป็นเพียงแค่การแสดงออกทางความคิดเท่านั้น มีความแตกต่างที่เกิดขึ้นจริงในพื้นที่นี้ นี่คือสคริปต์สั้น ๆ ที่ทดสอบสถานการณ์โดยใช้เชลล์ที่แตกต่างกันหลายตัว มันส่งออก IFS ที่แก้ไขแล้ว (ตั้งค่าเป็น:) ไปยังเชลล์ที่เรียกใช้จากนั้นพิมพ์ IFS เริ่มต้นของมัน

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

โดยทั่วไปแล้ว IFS ไม่ได้ถูกทำเครื่องหมายเพื่อการส่งออก แต่ถ้าเป็นเช่นนั้นให้สังเกตว่า bash, ksh93 และ mksh เพิกเฉยต่อสภาพแวดล้อมของพวกเขาIFS=:อย่างไรในขณะที่ขีดกลางและ busybox ให้เกียรติมัน

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

ข้อมูลรุ่นบางอย่าง:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

แม้ว่า bash, ksh93 และ mksh จะไม่เริ่มต้น IFS จากสภาพแวดล้อม แต่จะส่งออก IFS ที่แก้ไขใหม่

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


ฉันเห็นดังนั้นถ้าฉันสามารถถอดความมันเป็นเนื้อหาที่พกพาได้มากกว่าเพื่อระบุIFSค่าในสถานการณ์ส่วนใหญ่ที่จะใช้อย่างชัดเจนและบ่อยครั้งที่มันไม่ได้ผลมากนักที่จะพยายาม "รักษา" คุณค่าดั้งเดิมของมันไว้
Steven Lu

1
ปัญหาสำคัญคือถ้าสคริปต์ของคุณใช้ IFS ควรกำหนด / ยกเลิกการตั้งค่า IFS อย่างชัดเจนเพื่อให้แน่ใจว่าค่าของมันคือสิ่งที่คุณต้องการให้เป็น โดยปกติพฤติกรรมของสคริปต์ของคุณขึ้นอยู่กับไอเอฟเอหากมีการขยายพารามิเตอร์ unquoted, แทนคำสั่ง unquoted ขยาย unquoted เลขคณิตreads $*หรือการอ้างอิงยกมาสองครั้งเพื่อ รายการนั้นอยู่ด้านบนสุดของหัวของฉันดังนั้นจึงอาจไม่ครอบคลุม (โดยเฉพาะอย่างยิ่งเมื่อพิจารณาส่วนขยาย POSIX ของเชลล์สมัยใหม่)
Barefoot IO

10

โดยทั่วไปเป็นแนวปฏิบัติที่ดีในการคืนเงื่อนไขให้เป็นค่าเริ่มต้น

อย่างไรก็ตามในกรณีนี้ไม่มาก

ทำไม?:

  • ทุกครั้งที่เริ่มต้นสคริปต์ (ในทุบตี) ไอเอฟเอมีการตั้งค่า$' \t\n'
  • เพียงแค่การดำเนินการunset IFSทำหน้าที่เป็นถ้ามันถูกกำหนดให้เริ่มต้น

นอกจากนี้การจัดเก็บค่า IFS ก็มีปัญหาเช่นกัน
หาก IFS ดั้งเดิมไม่ได้ถูกตั้งรหัสIFS="$OldIFS"จะตั้ง IFS เป็น""ไม่ได้ยกเลิกมัน

หากต้องการรักษาค่าของ IFS (แม้ว่าจะไม่ได้ตั้งค่า) ให้ใช้สิ่งนี้:

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.

IFS ไม่สามารถยกเลิกการตั้งค่าได้ หากคุณไม่ได้ตั้งค่าเชลล์จะเปลี่ยนเป็นค่าเริ่มต้น ดังนั้นคุณไม่จำเป็นต้องตรวจสอบเรื่องนั้นเมื่อทำการบันทึก
filbranden

ระวังว่าในbash, unset IFSล้มเหลวในการยกเลิกการตั้งค่า IFS ถ้ามันได้รับการประกาศในท้องถิ่นในบริบทหลัก (บริบทของฟังก์ชั่น) และไม่ได้อยู่ในบริบทปัจจุบัน
Stéphane Chazelas

5

คุณมีสิทธิ์ที่จะลังเลเกี่ยวกับการปกปิดโลก ไม่ต้องกลัวมันเป็นไปได้ที่จะเขียนโค้ดการทำงานที่สะอาดโดยไม่ต้องแก้ไข global ที่แท้จริงIFSหรือทำการเต้นรำแบบ save / restore ที่ยุ่งยากและมีข้อผิดพลาด

คุณสามารถ:

  • ตั้งค่า IFS สำหรับการเรียกใช้ครั้งเดียว:

    IFS=value command_or_function

    หรือ

  • ตั้งค่า IFS ภายในเชลล์ย่อย:

    (IFS=value; statement)
    $(IFS=value; statement)

ตัวอย่าง

  • ในการรับสตริงที่คั่นด้วยจุลภาคจากอาร์เรย์:

    str="$(IFS=, ; echo "${array[*]-}")"

    หมายเหตุ: -เป็นเพียงการปกป้องอาร์เรย์ที่ว่างเปล่ากับset -uโดยการให้ค่าเริ่มต้นเมื่อไม่มีการตั้งค่า (ค่าที่เป็นสตริงที่ว่างเปล่าในกรณีนี้)

    IFSปรับเปลี่ยนใช้ได้เฉพาะภายใน subshell กลับกลายโดยแทนคำสั่ง$() นี่เป็นเพราะ subshells มีสำเนาของตัวแปรของเชลล์ที่อ้างถึงและสามารถอ่านค่าได้ แต่การแก้ไขใด ๆ ที่ทำโดย subshell จะส่งผลต่อการคัดลอกของ subshell เท่านั้นและไม่ใช่ตัวแปรของพาเรนต์

    คุณอาจกำลังคิดว่า: ทำไมไม่ข้าม subshell และทำสิ่งนี้:

    IFS=, str="${array[*]-}"  # Don't do this!

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

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    สุดท้ายเรามาอธิบายว่าทำไมตัวแปรนี้ถึงใช้งานไม่ได้:

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    echoคำสั่งจะจริงจะเรียกว่ากับIFSชุดตัวแปร,แต่ไม่ได้ดูแลหรือการใช้งานecho IFSความมหัศจรรย์ของการขยาย"${array[*]}"ไปยังสายอักขระถูกทำโดยเปลือก (sub-) ก่อนที่จะechoถูกเรียกใช้

  • หากต้องการอ่านทั้งไฟล์ (ที่ไม่มีNULLไบต์) เป็นตัวแปรเดียวชื่อVAR:

    IFS= read -r -d '' VAR < "${filepath}"

    หมายเหตุ: IFS=เหมือนกันIFS=""และIFS=''ทุกชุด IFS เป็นสตริงว่างซึ่งแตกต่างจากunset IFS: ถ้าIFSไม่ได้ตั้งค่าพฤติกรรมของฟังก์ชั่นทุบตีทั้งหมดที่ใช้ภายในIFSนั้นเหมือนกับว่าIFSมีค่าเริ่มต้น$' \t\n'เป็น

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

    กระบวนการ-d ''หรือ-d ""บอกให้อ่านเพื่อหยุดการเรียกใช้ปัจจุบันบนNULLไบต์แทนการขึ้นบรรทัดใหม่ตามปกติเท่านั้น

  • วิธีแยก$PATHตาม:ตัวคั่น:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    ตัวอย่างนี้แสดงให้เห็นอย่างหมดจด ในกรณีทั่วไปที่คุณแยกตามตัวคั่นอาจเป็นไปได้ที่แต่ละเขตข้อมูลจะมี (รุ่นที่ใช้ Escape) ตัวคั่นนั้น นึกถึงการพยายามอ่านแถวของ.csvไฟล์ที่คอลัมน์อาจมีเครื่องหมายจุลภาค (Escape หรืออ้างอิงด้วยวิธีใดวิธีหนึ่ง) ตัวอย่างข้างต้นจะไม่ทำงานตามที่ตั้งใจไว้สำหรับกรณีดังกล่าว

    ที่กล่าวว่าคุณจะมีโอกาสที่จะพบเช่น:-containing $PATHเส้นทางภายใน ในขณะที่ชื่อพา ธ UNIX / Linux ได้รับอนุญาตให้เก็บ a :ดูเหมือนว่า bash จะไม่สามารถจัดการกับพา ธดังกล่าวได้หากคุณพยายามที่จะเพิ่มลง$PATHในไฟล์ของคุณและเก็บไฟล์ที่สามารถใช้งานได้เนื่องจากไม่มีรหัสในการแยกวิเคราะห์ / โคลอนที่ยกมา : รหัสแหล่งที่มาของทุบตี 4.4

    ท้ายที่สุดโปรดทราบว่าส่วนย่อยจะเพิ่มบรรทัดใหม่ต่อท้ายไปยังองค์ประกอบสุดท้ายของอาร์เรย์ผลลัพธ์ (ดังที่เรียกโดย @ StéphaneChazelasในความคิดเห็นที่ถูกลบแล้วตอนนี้) และหากอินพุตเป็นสตริงว่างเอาต์พุตจะเป็นองค์ประกอบเดียว อาร์เรย์ที่องค์ประกอบจะประกอบด้วยบรรทัดใหม่ ( $'\n')

แรงจูงใจ

old_IFS="${IFS}"; command; IFS="${old_IFS}"วิธีการพื้นฐานที่สัมผัสทั่วโลกIFSจะทำงานตามที่คาดไว้สำหรับสคริปต์ที่ง่ายที่สุด อย่างไรก็ตามทันทีที่คุณเพิ่มความซับซ้อนใด ๆ ก็สามารถแยกออกได้อย่างง่ายดายและทำให้เกิดปัญหาที่ลึกซึ้ง:

  • หากcommandเป็นฟังก์ชั่นทุบตีที่ปรับเปลี่ยนโลกIFS(ทั้งโดยตรงหรือซ่อนจากมุมมองภายในฟังก์ชั่นอื่นที่มันเรียก) และในขณะที่ทำเช่นนั้นใช้ผิดพลาดทั่วโลกold_IFSตัวแปรเดียวกันในการบันทึก / กู้คืนคุณจะได้รับข้อผิดพลาด
  • เป็นแหลมออกในความคิดเห็นนี้โดย @Gillesถ้ารัฐเดิมIFSคือไม่มีการตั้งค่าไร้เดียงสาบันทึกและเรียกคืนจะไม่ทำงานและแม้จะส่งผลให้เกิดความล้มเหลวทันทีถ้าทั่วไป (ที่ผิด) ที่ใช้set -u(aka set -o nounset) ตัวเลือกเปลือก มีผลบังคับใช้
  • เป็นไปได้ที่เชลล์โค้ดบางตัวจะดำเนินการแบบอะซิงโครนัสกับโฟลว์การประมวลผลหลักเช่นกับตัวจัดการสัญญาณ (ดูhelp trap) หากรหัสนั้นปรับเปลี่ยนโลกIFSหรือสมมติว่ามันมีค่าเฉพาะคุณสามารถได้รับข้อบกพร่องที่ลึกซึ้ง

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

ข้อควรพิจารณาเพิ่มเติมสำหรับสคริปต์เหมือนไลบรารี

IFSโดยเฉพาะอย่างยิ่งความกังวลสำหรับผู้เขียนของเชลล์ฟังก์ชั่นห้องสมุดที่ต้องการให้แน่ใจว่ารหัสของพวกเขาทำงานอย่างมีประสิทธิภาพโดยไม่คำนึงถึงสถานะโลก ( IFS, ตัวเลือกเชลล์, ... ) ที่กำหนดโดยผู้เรียกร้องของพวกเขาและโดยไม่รบกวนสถานะนั้นเลย มันจะยังคงอยู่เสมอ)

เมื่อเขียนโค้ดไลบรารี่คุณไม่สามารถวางใจIFSได้ว่าจะมีค่าเฉพาะใด ๆ (ไม่ใช่แม้แต่ค่าดีฟอลต์) หรือแม้กระทั่งการตั้งค่าเลย แต่คุณจำเป็นต้องกำหนดอย่างชัดเจนIFSสำหรับตัวอย่างข้อมูลใด ๆ IFSที่มีลักษณะการทำงานขึ้นอยู่กับ

หากIFSมีการตั้งค่าไว้อย่างชัดเจนเป็นค่าที่จำเป็น (แม้ว่าจะเป็นค่าเริ่มต้น) ในทุกบรรทัดของรหัสที่ค่ามีความสำคัญโดยใช้กลไกใด ๆ ของทั้งสองที่อธิบายไว้ในคำตอบนี้มีความเหมาะสมที่จะ จำกัด ผลกระทบ เป็นอิสระจากรัฐทั่วโลกและหลีกเลี่ยงการปิดบังมันโดยสิ้นเชิง วิธีการนี้มีประโยชน์เพิ่มเติมในการทำให้ผู้ใช้ที่อ่านสคริปต์มีความIFSสำคัญอย่างยิ่งต่อคำสั่ง / การขยายตัวนี้อย่างแม่นยำด้วยค่าใช้จ่ายทางข้อความขั้นต่ำ (เปรียบเทียบกับแม้แต่การบันทึก / เรียกคืนพื้นฐานขั้นพื้นฐาน)

อะไรรหัสรับผลกระทบจากIFSหรือไม่?

โชคดีที่มีหลาย ๆ สถานการณ์ที่ไม่IFSสำคัญ (สมมติว่าคุณอ้างอิงการขยายของคุณเสมอ ):

  • "$*"และ"${array[*]}"การขยายตัว
  • การเรียกใช้งานของการreadกำหนดเป้าหมายหลายตัวแปรในตัว ( read VAR1 VAR2 VAR3) หรือตัวแปรอาร์เรย์ ( read -a ARRAY_VAR_NAME) ในตัว
  • การเรียกใช้การreadกำหนดเป้าหมายตัวแปรเดียวเมื่อมาถึงอักขระช่องว่างนำหน้า / ต่อท้ายหรืออักขระที่ไม่ใช่ช่องว่างปรากฏIFSขึ้น
  • การแยกคำ (เช่นสำหรับการขยายที่ไม่ได้กล่าวถึงซึ่งคุณอาจต้องการหลีกเลี่ยงเช่นกาฬโรค )
  • บางสถานการณ์ที่พบได้ทั่วไปน้อยกว่า (ดู: IFS @ Greg's Wiki )

ฉันไม่สามารถพูดได้ว่าฉันเข้าใจการแบ่ง $ PATH ตาม: ตัวคั่นโดยสมมติว่าไม่มีส่วนประกอบใด ๆ วิธีส่วนประกอบอาจมี:เมื่อ:เป็นตัวคั่น?
Stéphane Chazelas

@ StéphaneChazelasดี:เป็นตัวละครที่ถูกต้องเพื่อใช้ในชื่อไฟล์ในส่วน filesystems Unix / Linux :ดังนั้นจึงเป็นไปได้ทั้งหมดจะมีไดเรกทอรีที่มีชื่อที่มี บางทีเชลล์บางตัวมีข้อกำหนดเพื่อหลบหนี:ใน PATH โดยใช้สิ่งที่ต้องการ\:แล้วคุณจะเห็นคอลัมน์ที่ปรากฏซึ่งไม่ใช่ตัวคั่นจริง (ดูเหมือนว่าทุบตีไม่อนุญาตการหลบหนีเช่นนั้นฟังก์ชั่นระดับต่ำที่ใช้เมื่อวนซ้ำ$PATHค้นหา:ใน สตริง C: git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891 )
sls

ฉันแก้ไขคำตอบเพื่อหวังว่าจะทำให้$PATHตัวอย่างการแยกพร้อม:ชัดเจนยิ่งขึ้น
sls

1
ยินดีต้อนรับสู่ SO! ขอบคุณสำหรับคำตอบดังกล่าวในเชิงลึก :)
สตีเว่น Lu

1

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

เหตุใดจึงต้องเสี่ยงต่อการพิมพ์ผิดตั้งค่า IFS $' \t\n'เมื่อทุกสิ่งที่คุณต้องทำคือ

OIFS=$IFS
do_your_thing
IFS=$OIFS

หรือคุณสามารถเรียก subshell ถ้าคุณไม่ต้องการตั้งค่า / แก้ไขตัวแปรภายใน:

( IFS=:; do_your_thing; )

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