คำตอบทั้งหมดของคำถามนี้ผิดหรืออย่างใดอย่างหนึ่ง
ตอบผิด # 1
IFS=', ' read -r -a array <<< "$string"
1:$IFS
นี่คือทางที่ผิดของ ค่าของ$IFS
ตัวแปรไม่ได้ถูกใช้เป็นตัวคั่นสตริงความยาวตัวแปรเดียวแต่จะถูกนำมาเป็นชุดของตัวคั่นสตริงอักขระเดี่ยวโดยที่แต่ละฟิลด์ที่read
แยกออกจากบรรทัดอินพุตสามารถยกเลิกได้ด้วยอักขระใด ๆในชุด (เครื่องหมายจุลภาคหรือเว้นวรรคในตัวอย่างนี้)
ที่จริงแล้วสำหรับ sticklers จริงออกมีความหมายเต็มรูปแบบของ$IFS
มีส่วนเกี่ยวข้องมากขึ้นเล็กน้อย จากคู่มือทุบตี :
เชลล์ปฏิบัติต่ออักขระแต่ละตัวของIFSเป็นตัวคั่นและแยกผลลัพธ์ของการขยายตัวอื่น ๆ ออกเป็นคำโดยใช้อักขระเหล่านี้เป็นตัวยุติฟิลด์ หากIFSไม่ได้ตั้งค่าไว้หรือค่าของมันคือ<space><tab> <newline>ค่าเริ่มต้นตามด้วยลำดับของ<space> , <tab>และ<newline>ที่จุดเริ่มต้นและจุดสิ้นสุดของผลลัพธ์ของการขยายก่อนหน้า จะถูกละเว้นและลำดับของอักขระIFSใด ๆ ที่ไม่ได้อยู่ที่จุดเริ่มต้นหรือจุดสิ้นสุดจะทำหน้าที่กำหนดขอบเขตคำ หากIFSมีค่าอื่นที่ไม่ใช่ค่าเริ่มต้นดังนั้นลำดับของอักขระช่องว่าง<space> , <tab>และ <จะถูกละเว้นที่จุดเริ่มต้นและจุดสิ้นสุดของคำตราบใดที่อักขระช่องว่างอยู่ในค่าของIFS ( ตัวอักษรช่องว่างของIFS ) อักขระใด ๆ ในIFSที่ไม่ใช่ช่องว่างIFSพร้อมกับอักขระช่องว่างIFS ที่อยู่ติดกันใด ๆ จะคั่นเขตข้อมูล ลำดับของอักขระช่องว่างของIFSยังถือว่าเป็นตัวคั่น หากค่าของIFSเป็นโมฆะจะไม่เกิดการแบ่งคำ
โดยทั่วไปสำหรับการเริ่มต้นไม่ใช่ค่าที่ไม่ใช่ null ของ$IFS
เขตข้อมูลสามารถแยกกับทั้ง (1) ลำดับหนึ่งหรือมากกว่าหนึ่งตัวละครที่มีทั้งหมดมาจากชุดของ "ไอเอฟเอช่องว่างตัวอักษร" (นั่นคือที่แล้วแต่จำนวนใดของ<พื้นที่> , <tab>และ<newline> ("newline" หมายถึงการป้อนบรรทัด (LF) ) ปรากฏอยู่ที่ใดก็ได้$IFS
) หรือ (2) ไม่ใช่ "IFS whitespace character" ที่มีอยู่$IFS
พร้อมกับ "IFS whitespace character" ล้อมรอบ ในบรรทัดอินพุต
สำหรับ OP เป็นไปได้ว่าโหมดการแยกที่สองที่ฉันอธิบายไว้ในย่อหน้าก่อนหน้าเป็นสิ่งที่เขาต้องการสำหรับสตริงอินพุตของเขา แต่เรามั่นใจได้เลยว่าโหมดการแยกครั้งแรกที่ฉันอธิบายนั้นไม่ถูกต้องเลย ตัวอย่างเช่นถ้าสตริงอินพุตของเขาคือ'Los Angeles, United States, North America'
อะไร
IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")
2:แม้ว่าคุณจะใช้การแก้ปัญหานี้ด้วยการแยกตัวเดียว (เช่นเครื่องหมายจุลภาคด้วยตัวเองนั่นคือมีพื้นที่ต่อไปนี้หรือไม่สัมภาระอื่น ๆ ) ถ้าค่าของ$string
ตัวแปรที่เกิดขึ้นจะมี LFS ใด ๆ แล้วread
จะ หยุดการประมวลผลทันทีที่พบ LF ตัวแรก read
builtin เพียงกระบวนการหนึ่งบรรทัดต่อการภาวนา นี่คือความจริงแม้ว่าคุณจะเป็นท่อหรือเปลี่ยนเส้นทางการป้อนข้อมูลเฉพาะกับread
คำสั่งในขณะที่เรากำลังทำอยู่ในตัวอย่างนี้กับที่นี่สตริงกลไกและทำให้การป้อนข้อมูลที่ยังไม่ได้มีการประกันเพื่อจะหายไป รหัสที่ให้อำนาจread
builtin ไม่มีความรู้ของการไหลของข้อมูลภายในโครงสร้างคำสั่งที่มี
คุณสามารถยืนยันว่าสิ่งนี้ไม่น่าจะทำให้เกิดปัญหาได้ แต่ถึงกระนั้นมันก็เป็นอันตรายที่ละเอียดอ่อนที่ควรหลีกเลี่ยงหากเป็นไปได้ มันเกิดจากความจริงที่ว่าread
builtin จริง ๆ แล้วแบ่งระดับการป้อนข้อมูลสองระดับ: ก่อนเข้าสู่บรรทัดจากนั้นลงในฟิลด์ เนื่องจาก OP ต้องการเพียงหนึ่งระดับในการแยกการใช้read
builtin นี้จึงไม่เหมาะสมและเราควรหลีกเลี่ยง
3:ปัญหาที่อาจไม่ชัดเจนกับโซลูชันนี้คือread
ปล่อยเขตข้อมูลต่อท้ายเสมอหากไม่มีข้อมูลแม้ว่าจะรักษาเขตข้อมูลว่างไว้เป็นอย่างอื่น นี่คือตัวอย่าง:
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")
บางที OP ไม่สนใจเรื่องนี้ แต่ก็ยังมีข้อ จำกัด ที่ควรรู้ มันลดความทนทานและความมีชีวิตชีวาของโซลูชัน
ปัญหานี้สามารถแก้ไขได้โดยการเพิ่มตัวคั่นตัวต่อท้ายไปยังสตริงอินพุตก่อนที่จะป้อนมันread
ตามที่ฉันจะแสดงในภายหลัง
ตอบผิด # 2
string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
ความคิดที่คล้ายกัน:
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(หมายเหตุ: ฉันเพิ่มวงเล็บที่ขาดหายไปรอบ ๆ การทดแทนคำสั่งซึ่งผู้ตอบคำถามถูกละเว้น)
ความคิดที่คล้ายกัน:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
โซลูชันเหล่านี้ใช้ประโยชน์จากการแบ่งคำในการกำหนดอาร์เรย์เพื่อแยกสตริงออกเป็นฟิลด์ สนุกมากเช่นเดียวกับread
การแยกคำทั่วไปยังใช้$IFS
ตัวแปรพิเศษถึงแม้ว่าในกรณีนี้มันบอกเป็นนัยว่ามันถูกตั้งค่าเป็นค่าเริ่มต้นของ<space><tab> <newline>ดังนั้นลำดับใด ๆ ของ IFS หนึ่งหรือมากกว่านั้น อักขระ (ซึ่งเป็นอักขระช่องว่างทั้งหมดในขณะนี้) ถือเป็นตัวคั่นฟิลด์
วิธีนี้จะช่วยแก้ปัญหาการแยกสองระดับที่กระทำโดยread
เนื่องจากการแยกคำด้วยตัวเองถือเป็นการแบ่งระดับเดียวเท่านั้น แต่ก่อนหน้านี้ปัญหาที่นี่คือแต่ละฟิลด์ในสตริงอินพุตสามารถมี$IFS
อักขระได้อยู่แล้วดังนั้นพวกเขาจะแยกไม่ถูกต้องระหว่างการดำเนินการแยกคำ สิ่งนี้เกิดขึ้นไม่ใช่กรณีของสตริงอินพุตตัวอย่างใด ๆ ที่ผู้ตอบคำถามเหล่านี้ (สะดวกมาก ... ) แต่แน่นอนว่าจะไม่เปลี่ยนความจริงที่ว่ารหัสฐานใด ๆ ที่ใช้สำนวนนี้จะเสี่ยงต่อการ ระเบิดถ้าข้อสันนิษฐานนี้เคยถูกละเมิดในบางจุดลงบรรทัด ลองพิจารณาตัวอย่างของ'Los Angeles, United States, North America'
(หรือ'Los Angeles:United States:North America'
) ตัวอย่างของฉันอีกครั้ง
นอกจากนี้ยังมีการแยกคำที่ตามปกติโดยการขยายตัวของชื่อไฟล์ ( akaขยายตัวชื่อพา ธaka globbing) ซึ่งถ้าทำจะคำพูดที่อาจเกิดความเสียหายที่มีตัวละคร*
, ?
หรือ[
ตามด้วย]
(และถ้าextglob
มีการตั้งค่าเศษวงเล็บนำโดย?
, *
, +
, @
, หรือ!
) โดยการจับคู่พวกเขากับวัตถุระบบไฟล์และขยายคำ ("globs") ตาม คนแรกในสามผู้ตอบคำถามนี้ได้ตัดราคาปัญหานี้อย่างชาญฉลาดโดยการเรียกใช้set -f
ล่วงหน้าเพื่อปิดการใช้งานแบบวงกลม เทคนิคนี้ใช้งานได้ (แม้ว่าคุณควรจะเพิ่มset +f
หลังจากนั้นจะเปิดใช้งาน globbing อีกครั้งสำหรับโค้ดที่ตามมาซึ่งอาจขึ้นอยู่กับมัน) แต่ก็ไม่พึงประสงค์ที่จะต้องยุ่งกับการตั้งค่าเชลล์โลก
ปัญหาอีกข้อหนึ่งของคำตอบนี้ก็คือช่องว่างทั้งหมดจะหายไป นี่อาจเป็นปัญหาหรือไม่ขึ้นอยู่กับแอพพลิเคชั่น
หมายเหตุ: หากคุณกำลังจะใช้โซลูชันนี้จะเป็นการดีกว่าที่จะใช้${string//:/ }
รูปแบบ "การแทนที่รูปแบบ" ของการขยายพารามิเตอร์แทนที่จะไปที่ปัญหาในการเรียกใช้การทดแทนคำสั่ง (ซึ่งจะทำให้เชลล์) เริ่มต้นไพพ์ไลน์และ เรียกใช้งานปฏิบัติการภายนอก ( tr
หรือsed
) เนื่องจากการขยายพารามิเตอร์เป็นการดำเนินการภายในของเชลล์อย่างหมดจด (นอกจากนี้สำหรับtr
และsed
วิธีแก้ไขตัวแปรอินพุตควรถูกอ้างอิงสองครั้งภายในการแทนที่คำสั่งมิฉะนั้นการแยกคำจะมีผลในecho
คำสั่งและอาจยุ่งเหยิงกับค่าของฟิลด์นอกจากนี้รูปแบบเนื่องจากทำให้การซ้อนคำสั่งและ ช่วยให้การเน้นไวยากรณ์ดีขึ้นโดยเครื่องมือแก้ไขข้อความ)$(...)
รูปแบบของการแทนที่คำสั่งจะดีกว่าแบบเก่า`...`
ตอบผิด # 3
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
คำตอบนี้เป็นเกือบเหมือน# 2 ความแตกต่างคือผู้ตอบได้ทำการสันนิษฐานว่าเขตข้อมูลถูกคั่นด้วยอักขระสองตัวหนึ่งซึ่งหนึ่งในนั้นถูกแสดงในค่าเริ่มต้น$IFS
และอื่น ๆ ไม่ใช่ เขาได้แก้ไขกรณีที่ค่อนข้างเฉพาะนี้โดยการลบอักขระที่ไม่ใช่ IFS แทนโดยใช้การขยายการแทนที่รูปแบบแล้วใช้การแบ่งคำเพื่อแยกฟิลด์บนอักขระตัวแทน IFS ที่ยังมีชีวิตรอด
นี่ไม่ใช่วิธีแก้ปัญหาทั่วไป ยิ่งไปกว่านั้นมันอาจเป็นที่ถกเถียงกันอยู่ว่าเครื่องหมายจุลภาคนั้นเป็นตัวคั่น "หลัก" ที่นี่และการลอกและขึ้นอยู่กับอักขระช่องว่างสำหรับการแยกฟิลด์นั้นผิด อีกครั้งหนึ่งที่พิจารณา counterexample 'Los Angeles, United States, North America'
ฉัน:
ยิ่งไปกว่านั้นการขยายชื่อไฟล์อาจทำให้คำที่ขยายออกไปเสียหายได้ แต่สิ่งนี้สามารถป้องกันได้โดยการปิดใช้งานการวนซ้ำชั่วคราวสำหรับการมอบหมายด้วยset -f
แล้วset +f
นอกจากนี้ฟิลด์ว่างทั้งหมดจะสูญหายซึ่งอาจมีหรือไม่มีปัญหาขึ้นอยู่กับแอปพลิเคชัน
ตอบผิด # 4
string='first line
second line
third line'
oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"
สิ่งนี้คล้ายกับ# 2และ# 3ในการใช้การแบ่งคำเพื่อให้งานเสร็จสมบูรณ์ตอนนี้โค้ด$IFS
จะถูกตั้งค่าอย่างชัดเจนเพื่อให้มีเพียงตัวคั่นฟิลด์ตัวเดียวที่มีอยู่ในสตริงอินพุต ควรทำซ้ำว่าสิ่งนี้ไม่สามารถใช้ได้กับตัวคั่นเขตข้อมูลแบบหลายอักขระเช่นตัวคั่นพื้นที่จุลภาคของ OP แต่สำหรับตัวคั่นที่เป็นอักขระตัวเดียวเช่น LF ที่ใช้ในตัวอย่างนี้จริง ๆ แล้วใกล้เคียงกับความสมบูรณ์แบบ เขตข้อมูลไม่สามารถแบ่งกลางโดยไม่ได้ตั้งใจอย่างที่เราเห็นด้วยคำตอบที่ผิดก่อนหน้านี้และมีการแบ่งเพียงระดับเดียวตามที่ต้องการ
ปัญหาหนึ่งคือการขยายตัวของชื่อไฟล์จะคำได้รับผลกระทบเสียหายตามที่อธิบายไว้ก่อนหน้านี้แม้ว่าอีกครั้งนี้จะสามารถแก้ไขได้โดยการตัดงบที่สำคัญในการและset -f
set +f
อีกปัญหาที่อาจเกิดขึ้นคือว่าตั้งแต่ LF มีคุณสมบัติเป็น "ไอเอฟเอช่องว่างของตัวละคร" ตามที่กำหนดไว้ก่อนหน้านี้เขตข้อมูลที่ว่างเปล่าทั้งหมดจะหายไปเช่นเดียวกับใน# 2และ# 3 แน่นอนว่านี่จะไม่เป็นปัญหาหากตัวคั่นเกิดขึ้นเป็นตัวละครที่ไม่ใช่ "IFS whitespace" และขึ้นอยู่กับแอปพลิเคชั่นนั้นอาจไม่สำคัญ แต่อย่างใด
ดังนั้นเพื่อสรุปสมมติว่าคุณมีตัวคั่นหนึ่งตัวและมันก็เป็นอย่างใดอย่างหนึ่งที่ไม่ใช่ "ไอเอฟเอช่องว่างของตัวละคร" หรือคุณไม่สนใจเกี่ยวกับฟิลด์ที่ว่างเปล่าและคุณตัดงบที่สำคัญในการset -f
และset +f
แล้วการแก้ปัญหานี้ผลงาน แต่ไม่ใช่อย่างอื่น
(เพื่อประโยชน์ของข้อมูลการกำหนด LF ให้กับตัวแปรใน bash สามารถทำได้ง่ายขึ้นด้วย$'...'
ไวยากรณ์เช่นIFS=$'\n';
)
ตอบผิด # 5
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
ความคิดที่คล้ายกัน:
IFS=', ' eval 'array=($string)'
โซลูชันนี้มีการข้ามระหว่าง# 1 (อย่างที่ตั้ง$IFS
ไว้เป็นเครื่องหมายจุลภาค) และ# 2-4 (ซึ่งจะใช้การแบ่งคำเพื่อแยกสตริงออกเป็นฟิลด์) ด้วยเหตุนี้มันได้รับความทุกข์ทรมานจากปัญหาส่วนใหญ่ที่ทำให้คำตอบที่ผิดทั้งหมดข้างต้นเรียงลำดับเหมือนเลวร้ายที่สุดของโลก
นอกจากนี้เกี่ยวกับตัวแปรที่สองมันอาจดูเหมือนว่าการeval
โทรนั้นไม่จำเป็นอย่างสมบูรณ์เนื่องจากอาร์กิวเมนต์เป็นสตริงตัวอักษรที่ยกมาเดี่ยวและดังนั้นจึงเป็นที่รู้จักกันแบบคงที่ แต่จริงๆแล้วมีประโยชน์ที่ไม่ชัดเจนในการใช้eval
วิธีนี้ โดยปกติเมื่อคุณรันคำสั่งง่าย ๆ ซึ่งประกอบด้วยการกำหนดตัวแปรเท่านั้นความหมายโดยไม่มีคำสั่งจริงตามมาการกำหนดจะมีผลในสภาพแวดล้อมของเชลล์:
IFS=', '; ## changes $IFS in the shell environment
สิ่งนี้เป็นจริงแม้ว่าคำสั่งพื้นฐานจะเกี่ยวข้องกับการกำหนดตัวแปรหลายตัว อีกครั้งตราบใดที่ไม่มีคำสั่งการกำหนดตัวแปรทั้งหมดจะมีผลกับสภาพแวดล้อมของเชลล์:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
แต่หากการกำหนดตัวแปรแนบมากับชื่อคำสั่ง (ฉันชอบเรียกสิ่งนี้ว่า "การกำหนดคำนำหน้า") มันจะไม่ส่งผลกระทบต่อสภาพแวดล้อมของเชลล์และจะส่งผลกระทบต่อสภาพแวดล้อมของคำสั่งที่เรียกใช้แทนเท่านั้น หรือภายนอก:
IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it
คำพูดที่เกี่ยวข้องจากคู่มือทุบตี :
หากไม่มีผลลัพธ์ชื่อคำสั่งการกำหนดตัวแปรจะมีผลกับสภาพแวดล้อมเชลล์ปัจจุบัน มิฉะนั้นตัวแปรจะถูกเพิ่มเข้ากับสภาพแวดล้อมของคำสั่งที่ดำเนินการและจะไม่ส่งผลกระทบต่อสภาพแวดล้อมของเชลล์ปัจจุบัน
เป็นไปได้ที่จะใช้ประโยชน์จากคุณสมบัติของการกำหนดตัวแปรนี้เพื่อเปลี่ยนแปลง$IFS
เพียงชั่วคราวซึ่งช่วยให้เราหลีกเลี่ยงกลเม็ดการบันทึกและกู้คืนทั้งหมดเช่นเดียวกับที่ทำกับ$OIFS
ตัวแปรในตัวแปรแรก แต่ความท้าทายที่เราเผชิญอยู่ที่นี่คือคำสั่งที่เราต้องเรียกใช้นั้นเป็นเพียงการกำหนดตัวแปรเท่านั้นและด้วยเหตุนี้มันจึงไม่เกี่ยวข้องกับคำสั่งเพื่อทำการ$IFS
มอบหมายชั่วคราว คุณอาจคิดกับตัวเองว่าทำไมไม่เพียงแค่เพิ่มคำสั่ง no-op ลงในคำสั่งอย่างเช่น: builtin
เพื่อทำการ$IFS
มอบหมายชั่วคราว สิ่งนี้ใช้ไม่ได้เพราะจะทำให้การ$array
บ้านทำงานชั่วคราวเช่นกัน:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
ดังนั้นเราจึงได้อย่างมีประสิทธิภาพที่อับจนเล็กน้อย -22 แต่เมื่อeval
เรียกใช้รหัสมันจะทำงานในสภาพแวดล้อมของเชลล์ราวกับว่ามันเป็นปกติซอร์สโค้ดแบบคงที่และดังนั้นเราจึงสามารถเรียกใช้การ$array
มอบหมายภายในeval
อาร์กิวเมนต์เพื่อให้มันมีผลในสภาพแวดล้อมของเชลล์ในขณะที่การ$IFS
กำหนดคำนำหน้านั้น ถูกนำหน้าไปยังeval
คำสั่งจะไม่อยู่ได้นานกว่าeval
คำสั่ง นี่เป็นเคล็ดลับที่ใช้ในชุดที่สองของโซลูชันนี้:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does
ดังที่คุณเห็นจริง ๆ แล้วมันเป็นกลอุบายที่ฉลาดและบรรลุสิ่งที่ต้องการ (อย่างน้อยก็เกี่ยวกับผลกระทบของการมอบหมาย) ในลักษณะที่ค่อนข้างไม่ชัดเจน ที่จริงแล้วฉันไม่ได้ต่อต้านเคล็ดลับนี้โดยทั่วไปแม้จะมีส่วนร่วมของeval
; เพียงแค่ระมัดระวังในการอ้างสตริงอาร์กิวเมนต์เพื่อป้องกันภัยคุกคามความปลอดภัย
แต่อีกครั้งเนื่องจากการรวมตัวกันของปัญหา "ที่เลวร้ายที่สุดของโลก" นี่ยังคงเป็นคำตอบที่ผิดสำหรับความต้องการของ OP
ตอบผิด # 6
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
อืม ... อะไรนะ? OP มีตัวแปรสตริงที่ต้องแยกวิเคราะห์ในอาร์เรย์ "คำตอบ" นี้เริ่มต้นด้วยเนื้อหาคำต่อคำของสตริงอินพุตที่วางลงในตัวอักษรอาร์เรย์ ฉันเดาว่าเป็นวิธีหนึ่งที่จะทำ
ดูเหมือนว่าผู้ตอบอาจสันนิษฐานว่า$IFS
ตัวแปรมีผลต่อการแยกวิเคราะห์ทุบตีทั้งหมดในบริบททั้งหมดซึ่งไม่เป็นความจริง จากคู่มือทุบตี:
IFS The Internal Field Separator ที่ใช้สำหรับการแยกคำหลังการขยายและเพื่อแยกบรรทัดเป็นคำด้วยคำสั่งread builtin ค่าเริ่มต้นคือ<พื้นที่> <แท็บ> <newline>
ดังนั้น$IFS
ตัวแปรพิเศษจึงใช้ในบริบทที่สองเท่านั้น: (1) การแยกคำที่ดำเนินการหลังจากการขยาย (หมายถึงไม่ได้เมื่อแยกวิเคราะห์ซอร์สโค้ด bash) และ (2) สำหรับการแยกบรรทัดอินพุตเป็นคำโดยread
builtin
ให้ฉันพยายามทำให้ชัดเจนขึ้น ฉันคิดว่ามันอาจจะดีที่จะดึงความแตกต่างระหว่างการแยกและการดำเนินการ Bash ต้องแยกวิเคราะห์ซอร์สโค้ดซึ่งเห็นได้ชัดว่าเป็นเหตุการณ์การแยกวิเคราะห์และจากนั้นจะรันโค้ดซึ่งภายหลังเมื่อมีการขยายเข้ามาในรูปภาพ การขยายตัวเป็นเหตุการณ์การดำเนินการจริงๆ นอกจากนี้ฉันยังมีปัญหากับคำอธิบายของ$IFS
ตัวแปรที่ฉันเพิ่งยกมาข้างต้น; แทนที่จะพูดว่าการแยกคำนั้นเกิดขึ้นหลังจากการขยายตัวฉันจะบอกว่าการแยกคำนั้นเกิดขึ้นในระหว่างการขยายหรืออาจจะแม่นยำกว่านั้นการแยกคำนั้นเป็นส่วนหนึ่งของกระบวนการขยายตัว วลี "การแยกคำ" หมายถึงขั้นตอนการขยายตัวนี้เท่านั้น มันไม่ควรถูกใช้เพื่ออ้างถึงการแยกวิเคราะห์ของซอร์สโค้ดทุบตีแม้ว่าน่าเสียดายที่เอกสารทำดูเหมือนจะโยนคำว่า "แยก" และ "คำ" มาก นี่คือข้อความที่ตัดตอนมาที่เกี่ยวข้องจากคู่มือlinux.die.netของเวอร์ชันทุบตี:
การขยายจะดำเนินการในบรรทัดคำสั่งหลังจากที่มันถูกแบ่งออกเป็นคำ มีหลายชนิดที่เจ็ดของการขยายตัวจะดำเนินการ: การขยายตัวรั้ง , ตัวหนอนขยายตัว , พารามิเตอร์และการขยายตัวตัวแปร , แทนคำสั่ง , การขยายตัวทางคณิตศาสตร์ , แยกคำและการขยายตัวของพา ธ
คำสั่งของการขยายคือ: การขยายรั้ง; การขยายตัวของลูกอัลเดอร์พารามิเตอร์และการขยายตัวแปรการขยายเลขคณิตและการทดแทนคำสั่ง (ทำได้จากซ้ายไปขวา) การแยกคำ และการขยายชื่อพา ธ
คุณสามารถโต้แย้งว่ารุ่น GNUของคู่มือทำได้ดีกว่าเล็กน้อยเนื่องจากมันเลือกคำว่า "โทเค็น" แทนที่จะเป็น "คำ" ในประโยคแรกของส่วนขยาย:
การขยายจะดำเนินการในบรรทัดคำสั่งหลังจากที่มันถูกแบ่งออกเป็นโทเค็น
จุดสำคัญคือ$IFS
ไม่เปลี่ยนวิธีการแยกวิเคราะห์รหัสที่มาทุบตี การแยกวิเคราะห์ซอร์สโค้ดของ bash นั้นแท้จริงแล้วเป็นกระบวนการที่ซับซ้อนมากซึ่งเกี่ยวข้องกับการรับรู้องค์ประกอบต่าง ๆ ของไวยากรณ์เชลล์เช่นลำดับคำสั่ง, รายการคำสั่ง, ไพพ์ไลน์, การขยายพารามิเตอร์, การแทนที่ทางคณิตศาสตร์และการแทนที่คำสั่ง ส่วนใหญ่กระบวนการแยกวิเคราะห์ bash ไม่สามารถเปลี่ยนแปลงได้โดยการกระทำระดับผู้ใช้เช่นการกำหนดตัวแปร (อันที่จริงมีข้อยกเว้นเล็กน้อยบางอย่างสำหรับกฎนี้ตัวอย่างเช่นดูการตั้งค่าเชลล์ต่างๆcompatxx
ซึ่งสามารถเปลี่ยนลักษณะบางอย่างของการแยกวิเคราะห์พฤติกรรมแบบทันทีทันใด) "คำพูด" / "โทเค็น" ต้นน้ำที่เป็นผลมาจากขั้นตอนการแยกวิเคราะห์ที่ซับซ้อนนี้จะขยายออกไปตามกระบวนการทั่วไปของ "การขยายตัว" ที่แยกย่อยลงในข้อความที่ตัดตอนมาจากเอกสารข้างต้นที่แยกคำขยายข้อความ คำเป็นเพียงขั้นตอนเดียวของกระบวนการนั้น การแยกคำจะสัมผัสกับข้อความที่แยกออกจากขั้นตอนการขยายก่อนหน้าเท่านั้น มันไม่ส่งผลกระทบต่อข้อความตัวอักษรที่แยกวิเคราะห์ทันที bytestream แหล่งที่มา
ตอบผิด # 7
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
นี่คือหนึ่งในโซลูชั่นที่ดีที่สุด โปรดสังเกตว่าเรากลับมาใช้read
อีกครั้ง ฉันไม่ได้บอกก่อนหน้านี้ว่าread
ไม่เหมาะสมเพราะมันแบ่งออกเป็นสองระดับเมื่อเราต้องการเพียงหนึ่งเดียว เคล็ดลับที่นี่คือคุณสามารถโทรออกread
ในลักษณะที่แยกได้เพียงระดับเดียวอย่างมีประสิทธิภาพโดยแยกออกจากหนึ่งช่องต่อการเรียกเท่านั้นซึ่งจำเป็นต้องเสียค่าใช้จ่ายในการโทรซ้ำ ๆ กันเป็นวง มันเป็นมือที่คล่องแคล่ว แต่มันใช้งานได้ดี
แต่มีปัญหา ก่อน: เมื่อคุณระบุอาร์กิวเมนต์NAMEอย่างน้อยหนึ่งรายการอาร์กิวเมนต์read
นั้นจะละเว้นช่องว่างนำหน้าและต่อท้ายในแต่ละฟิลด์ที่แยกออกจากสตริงป้อนข้อมูลโดยอัตโนมัติ สิ่งนี้เกิดขึ้นไม่ว่าจะ$IFS
ถูกตั้งค่าเป็นค่าเริ่มต้นหรือไม่ตามที่อธิบายไว้ก่อนหน้าในโพสต์นี้ ตอนนี้ OP อาจไม่สนใจเรื่องนี้สำหรับกรณีการใช้งานเฉพาะของเขาและในความเป็นจริงมันอาจเป็นคุณสมบัติที่ต้องการของพฤติกรรมการแยกวิเคราะห์ แต่ไม่ใช่ทุกคนที่ต้องการแยกสตริงลงในฟิลด์จะต้องการสิ่งนี้ อย่างไรก็ตามมีวิธีแก้ไข: การใช้งานที่ค่อนข้างไม่ชัดเจนread
คือส่งผ่านอาร์กิวเมนต์NAMEเป็นศูนย์ ในกรณีนี้read
จะเก็บบรรทัดอินพุตทั้งหมดที่ได้รับจากอินพุตสตรีมในตัวแปรที่มีชื่อ$REPLY
และเป็นโบนัสมันจะไม่ตัดส่วนนำและช่องว่างต่อท้ายออกจากค่า นี่เป็นการใช้งานที่มีประสิทธิภาพอย่างมากread
ซึ่งฉันได้ใช้ประโยชน์จากอาชีพการเขียนโปรแกรมเชลล์ของฉันบ่อยครั้ง นี่คือตัวอย่างของความแตกต่างในพฤติกรรม:
string=$' a b \n c d \n e f '; ## input string
a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a b" [1]="c d" [2]="e f") ## read trimmed surrounding whitespace
a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]=" a b " [1]=" c d " [2]=" e f ") ## no trimming
ปัญหาที่สองกับการแก้ปัญหานี้คือมันไม่ได้อยู่ในกรณีของตัวคั่นฟิลด์ที่กำหนดเองเช่นจุลภาคของพื้นที่ OP เหมือนก่อนหน้านี้ไม่รองรับตัวคั่นหลายตัวซึ่งเป็นข้อ จำกัด ที่โชคร้ายของโซลูชันนี้ เราสามารถพยายามแบ่งจุลภาคอย่างน้อยโดยระบุตัวคั่นให้กับ-d
ตัวเลือก แต่ดูว่าเกิดอะไรขึ้น:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")
คาดการณ์ได้ช่องว่างรอบข้างที่ไม่ได้นับถูกดึงเข้าไปในค่าฟิลด์และด้วยเหตุนี้สิ่งนี้จะต้องได้รับการแก้ไขในภายหลังผ่านการดำเนินการตัดแต่ง (สิ่งนี้สามารถทำได้โดยตรงใน while-loop) แต่มีข้อผิดพลาดอื่นที่ชัดเจน: ยุโรปหายไป! เกิดอะไรขึ้นกับมัน? คำตอบก็คือread
ส่งคืนรหัสส่งคืนที่ล้มเหลวหากพบจุดสิ้นสุดไฟล์ (ในกรณีนี้เราสามารถเรียกได้ว่าเป็นจุดสิ้นสุดสตริง) โดยไม่ต้องพบกับจุดสิ้นสุดฟิลด์สุดท้ายในฟิลด์สุดท้าย นี่เป็นสาเหตุให้ while-loop หยุดก่อนเวลาอันควรและเราจะสูญเสียสนามสุดท้าย
ในทางเทคนิคแล้วข้อผิดพลาดเดียวกันนี้ทำให้ตัวอย่างก่อนหน้านี้เสียหายเช่นกัน ความแตกต่างก็คือตัวคั่นฟิลด์ถูกนำมาเป็น LF ซึ่งเป็นค่าเริ่มต้นเมื่อคุณไม่ได้ระบุ-d
ตัวเลือกและ<<<
กลไก ("here-string") จะผนวก LF เข้ากับสตริงโดยอัตโนมัติก่อนที่จะป้อนเป็น อินพุตให้กับคำสั่ง ดังนั้นในกรณีเหล่านี้เราจัดเรียงโดยไม่ตั้งใจแก้ปัญหาของลดลงสนามสุดท้ายโดยไม่เจตนาท้าย Terminator หุ่นเพิ่มเติมในการป้อนข้อมูล ลองเรียกวิธีนี้แก้ปัญหา "dummy-terminator" เราสามารถใช้วิธีแก้ปัญหาตัวจำลองแบบแมนนวลสำหรับตัวคั่นแบบกำหนดเองใด ๆ โดยเชื่อมต่อกับสตริงอินพุตของเราเมื่อทำการอินสแตนซ์ที่นี่ - สตริง:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
มีปัญหาแก้ไข อีกวิธีการหนึ่งคือการทำลาย while-loop ถ้าทั้งสอง (1) read
กลับมาล้มเหลวและ (2) $REPLY
ว่างเปล่าความหมายread
ไม่สามารถอ่านตัวอักษรใด ๆ ก่อนที่จะกดปุ่มสิ้นสุดไฟล์ การสาธิต:
a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
วิธีการนี้ยังเปิดเผย LF ที่เป็นความลับซึ่งจะผนวกเข้ากับสตริงที่นี่โดยอัตโนมัติ <<<
ผู้ดำเนินการเปลี่ยนเส้นทาง แน่นอนว่ามันสามารถแยกออกจากกันได้ผ่านการดำเนินการตัดทอนอย่างชัดเจนตามที่อธิบายไว้เมื่อสักครู่ที่ผ่านมา แต่เห็นได้ชัดว่าวิธีการใช้ตัวจำลองแบบเทอร์มินัลด้วยตนเองแก้ได้โดยตรงดังนั้นเราจึงสามารถทำได้ โซลูชันดัมมี่ - เทอร์มิเนเตอร์แบบแมนนวลนั้นค่อนข้างสะดวกในการที่จะแก้ปัญหาทั้งสองนี้ (ปัญหาที่เกิดจากการตกจากพื้นและปัญหาต่อท้าย - LF) ในครั้งเดียว
โดยรวมแล้วนี่เป็นวิธีแก้ปัญหาที่ทรงพลัง มันเป็นเพียงจุดอ่อนที่เหลืออยู่คือการขาดการสนับสนุนสำหรับตัวคั่นหลายตัวซึ่งฉันจะอยู่ในภายหลัง
ตอบผิด # 8
string='first line
second line
third line'
readarray -t lines <<<"$string"
(อันที่จริงแล้วมาจากโพสต์เดียวกันกับ# 7 ; ผู้ตอบตอบได้แก้ปัญหาสองข้อในโพสต์เดียวกัน)
readarray
builtin ซึ่งเป็นคำพ้องสำหรับmapfile
เหมาะ มันเป็นคำสั่ง builtin ซึ่งแยกวิเคราะห์ bytestream เป็นตัวแปรอาร์เรย์ในนัดเดียว; ไม่ยุ่งกับลูปเงื่อนไขการเปลี่ยนตัวหรือสิ่งอื่นใด และไม่ตัดแถบช่องว่างใด ๆ ออกจากสตริงอินพุตอย่างลับๆ และ (ถ้า-O
ไม่ได้รับ) มันจะทำการล้างอาเรย์เป้าหมายอย่างสะดวกก่อนที่จะทำการกำหนด แต่มันก็ยังไม่สมบูรณ์แบบดังนั้นคำวิจารณ์ของฉันจึงเป็นคำตอบที่ผิด
ก่อนอื่นเพียงเพื่อให้ได้สิ่งนี้ออกมาโปรดทราบว่าเช่นเดียวกับพฤติกรรมของread
การแยกวิเคราะห์ฟิลด์ให้readarray
วางฟิลด์ต่อท้ายหากว่างเปล่า นี่อาจไม่ใช่ข้อกังวลของ OP แต่อาจเป็นกรณีการใช้งานบางอย่าง ฉันจะกลับมาที่นี่อีกสักครู่
ประการที่สองเหมือนก่อนหน้านี้ไม่รองรับตัวคั่นหลายตัว ฉันจะแก้ไขปัญหานี้ในชั่วขณะหนึ่งเช่นกัน
ประการที่สามการแก้ปัญหาตามที่เขียนไม่ได้แยกสตริงการป้อนข้อมูลของ OP และในความเป็นจริงมันไม่สามารถใช้ตาม - คือการแยกมัน ฉันจะขยายในขณะนี้เช่นกัน
ด้วยเหตุผลข้างต้นฉันยังถือว่านี่เป็น "คำตอบที่ผิด" สำหรับคำถามของ OP ด้านล่างฉันจะให้สิ่งที่ฉันคิดว่าเป็นคำตอบที่ถูก
คำตอบที่ถูกต้อง
นี่คือความพยายามที่ไร้เดียงสาที่จะทำให้# 8ทำงานได้โดยเพียงระบุ-d
ตัวเลือก:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
เราเห็นผลเป็นเหมือนผลที่เราได้จากวิธีการสองครั้งตามเงื่อนไขของบ่วงread
วิธีการแก้ปัญหาที่กล่าวไว้ใน# 7 เราเกือบจะสามารถแก้ปัญหานี้ได้ด้วยเคล็ดลับ dummy-terminator:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
ปัญหาที่นี่คือที่readarray
เก็บรักษาเขตข้อมูลต่อท้ายเนื่องจากตัว<<<
ดำเนินการเปลี่ยนเส้นทางต่อท้าย LF กับสายป้อนดังนั้นเขตข้อมูลต่อท้ายไม่ว่างเปล่า (มิฉะนั้นมันจะถูกทิ้ง) เราสามารถจัดการสิ่งนี้ได้โดยการยกเลิกการตั้งค่าองค์ประกอบอาร์เรย์สุดท้ายหลังจากข้อเท็จจริง:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
ปัญหาเพียงสองปัญหาที่ยังคงมีอยู่ซึ่งมีความเกี่ยวข้องกันจริงๆคือ (1) พื้นที่ว่างภายนอกที่ต้องถูกตัดแต่งและ (2) ขาดการสนับสนุนสำหรับตัวคั่นหลายตัว
แน่นอนว่าช่องว่างสามารถถูกตัดออกได้ในภายหลัง (ตัวอย่างเช่นดูวิธีการตัดช่องว่างจากตัวแปร Bash ได้อย่างไร ) แต่ถ้าเราสามารถแฮ็คตัวคั่นหลายตัวได้นั่นจะช่วยแก้ปัญหาทั้งสองในนัดเดียว
น่าเสียดายที่ไม่มีวิธีการโดยตรงเพื่อให้ตัวคั่นหลายตัวทำงาน ทางออกที่ดีที่สุดที่ฉันคิดคือการประมวลผลสตริงอินพุตล่วงหน้าเพื่อแทนที่ตัวคั่นแบบหลายอักขระด้วยตัวคั่นอักขระเดี่ยวที่จะรับประกันได้ว่าจะไม่ชนกับเนื้อหาของสตริงอินพุต เพียงตัวละครที่มีการรับประกันนี้เป็นไบต์ NUL นี่เป็นเพราะใน bash (แม้ว่าไม่ใช่ใน zsh, โดยบังเอิญ) ตัวแปรไม่สามารถมี NUL byte ขั้นตอนการประมวลผลล่วงหน้านี้สามารถทำได้แบบอินไลน์ในการทดแทนกระบวนการ นี่คือวิธีการใช้awk :
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
ในที่สุด! วิธีการแก้ปัญหานี้จะไม่แยกฟิลด์ที่อยู่ตรงกลางอย่างผิดพลาดจะไม่ถูกตัดออกก่อนเวลาอันควรจะไม่ปล่อยฟิลด์ว่างเปล่าจะไม่ทำให้ตัวเองเสียหายในการขยายชื่อไฟล์จะไม่ดึงแถบชั้นนำ ไม่ต้องการลูปและไม่ได้ชำระสำหรับตัวคั่นอักขระเดียว
ตัดแต่งน้ำยา
สุดท้ายผมอยากจะแสดงให้เห็นถึงวิธีการแก้ปัญหาของตัวเองค่อนข้างซับซ้อนตัดแต่งของฉันโดยใช้ปิดบังตัวเลือกในการ-C callback
readarray
แต่น่าเสียดายที่ฉันมีจำนวน จำกัด เกิน 30,000 ตัวอักษรของ Stack Overflow ของตัวอักษร Stack Overflow ดังนั้นฉันจะไม่สามารถอธิบายได้ ฉันจะปล่อยให้มันเป็นแบบฝึกหัดสำหรับผู้อ่าน
function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
,
(เครื่องหมายจุลภาค) ไม่ใช่อักขระเดียวเช่นเครื่องหมายจุลภาค หากคุณสนใจ แต่เพียงผู้เดียวคำตอบที่นี่จะง่ายต่อการติดตาม: stackoverflow.com/questions/918886//