แยกสตริงออกเป็นอาร์เรย์ใน Bash


640

ในสคริปต์ Bash ฉันต้องการแยกบรรทัดออกเป็นชิ้น ๆ และเก็บไว้ในอาร์เรย์

เส้น:

Paris, France, Europe

ฉันต้องการที่จะมีพวกเขาในอาร์เรย์เช่นนี้:

array[0] = Paris
array[1] = France
array[2] = Europe

ฉันต้องการใช้รหัสอย่างง่ายความเร็วของคำสั่งไม่สำคัญ ฉันจะทำมันได้อย่างไร


22
นี่เป็น # 1 Google ที่ได้รับความนิยม แต่มีข้อโต้แย้งในคำตอบเนื่องจากคำถามที่ถามมาเกี่ยวกับการกำหนดขอบเขต, (เครื่องหมายจุลภาค) ไม่ใช่อักขระเดียวเช่นเครื่องหมายจุลภาค หากคุณสนใจ แต่เพียงผู้เดียวคำตอบที่นี่จะง่ายต่อการติดตาม: stackoverflow.com/questions/918886//
antak

หากคุณต้องการที่จะลบล้างสตริงและไม่สนใจที่จะให้มันเป็นอาร์เรย์cutเป็นคำสั่ง bash ที่มีประโยชน์ที่ต้องคำนึงถึงด้วย ตัวแยกสามารถระบุได้en.wikibooks.org/wiki/Cutนอกจากนี้คุณยังสามารถแยกข้อมูลออกจากโครงสร้างบันทึกความกว้างคงที่ได้เช่นกัน en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

คำตอบ:


1088
IFS=', ' read -r -a array <<< "$string"

โปรดทราบว่าตัวละครใน$IFSได้รับการรักษาเป็นรายบุคคลเป็นตัวคั่นเพื่อที่ว่าในกรณีนี้เขตอาจจะแยกจากกันโดยทั้งจุลภาคหรือพื้นที่มากกว่าลำดับของตัวละครทั้งสอง ที่น่าสนใจว่าจะไม่มีการสร้างเขตข้อมูลว่างเมื่อเครื่องหมายจุลภาค - ช่องว่างในอินพุตเนื่องจากพื้นที่ได้รับการปฏิบัติเป็นพิเศษ

ในการเข้าถึงองค์ประกอบแต่ละรายการ:

echo "${array[0]}"

ในการวนซ้ำองค์ประกอบต่างๆ:

for element in "${array[@]}"
do
    echo "$element"
done

รับทั้งดัชนีและค่า:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

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

unset "array[1]"
array[42]=Earth

ในการรับจำนวนองค์ประกอบในอาเรย์:

echo "${#array[@]}"

ดังกล่าวข้างต้นอาร์เรย์อาจเบาบางดังนั้นคุณไม่ควรใช้ความยาวเพื่อรับองค์ประกอบสุดท้าย นี่คือวิธีที่คุณสามารถทำได้ใน Bash 4.2 และใหม่กว่า:

echo "${array[-1]}"

ใน Bash รุ่นใด ๆ (จากที่อื่นหลังจาก 2.05b):

echo "${array[@]: -1:1}"

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


15
เพียงใช้IFS=', 'แล้วคุณไม่ต้องลบช่องว่างแยกต่างหาก ทดสอบ:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: ขอบคุณ ฉันไม่รู้ว่าฉันคิดอะไรอยู่ ฉันชอบที่จะใช้declare -p arrayสำหรับผลการทดสอบโดยวิธี
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

1
ดูเหมือนจะไม่เคารพคำพูด เช่นFrance, Europe, "Congo, The Democratic Republic of the"นี้จะแยกหลังจากคองโก
Yisrael Dov

2
@YisraelDov: Bash ไม่มีวิธีจัดการกับ CSV ด้วยตัวเอง ไม่สามารถบอกความแตกต่างระหว่างเครื่องหมายจุลภาคภายในเครื่องหมายคำพูดกับคำพูดภายนอก คุณจะต้องใช้เครื่องมือที่เข้าใจ CSV เช่น lib ในภาษาระดับสูงกว่าเช่นโมดูลcsvใน Python
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"จะแยกarray=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")เป็นบันทึกย่อ ดังนั้นจึงใช้ได้กับฟิลด์ที่ไม่มีช่องว่างเท่านั้นเนื่องจากIFS=', 'เป็นชุดของอักขระแต่ละตัว - ไม่ใช่ตัวคั่นสตริง
dawg

332

คำตอบทั้งหมดของคำถามนี้ผิดหรืออย่างใดอย่างหนึ่ง


ตอบผิด # 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 ตัวแรก readbuiltin เพียงกระบวนการหนึ่งบรรทัดต่อการภาวนา นี่คือความจริงแม้ว่าคุณจะเป็นท่อหรือเปลี่ยนเส้นทางการป้อนข้อมูลเฉพาะกับreadคำสั่งในขณะที่เรากำลังทำอยู่ในตัวอย่างนี้กับที่นี่สตริงกลไกและทำให้การป้อนข้อมูลที่ยังไม่ได้มีการประกันเพื่อจะหายไป รหัสที่ให้อำนาจreadbuiltin ไม่มีความรู้ของการไหลของข้อมูลภายในโครงสร้างคำสั่งที่มี

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

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 -fset +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) สำหรับการแยกบรรทัดอินพุตเป็นคำโดยreadbuiltin

ให้ฉันพยายามทำให้ชัดเจนขึ้น ฉันคิดว่ามันอาจจะดีที่จะดึงความแตกต่างระหว่างการแยกและการดำเนินการ 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 ; ผู้ตอบตอบได้แก้ปัญหาสองข้อในโพสต์เดียวกัน)

readarraybuiltin ซึ่งเป็นคำพ้องสำหรับ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")

8
นอกจากนี้ยังอาจเป็นประโยชน์ในการบันทึก (แม้ว่าคุณจะไม่มีที่ว่างพอที่จะทำเช่นนั้น) ว่า-dตัวเลือกสำหรับการreadarrayปรากฏครั้งแรกใน Bash 4.4
fbicknel

2
คำตอบที่ดี (+1) หากคุณเปลี่ยน awk ของคุณเป็นawk '{ gsub(/,[ ]+|$/,"\0"); print }'และกำจัด concatenation ของรอบชิงชนะเลิศ", " นั้นคุณไม่ต้องไปที่ยิมนาสติกในการกำจัดบันทึกสุดท้าย ดังนั้น: ในทุบตีที่สนับสนุนreadarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string") readarrayหมายเหตุ: วิธีการของคุณคือทุบตี 4.4 ขึ้นไปผมคิดว่าเพราะ-dในreadarray
dawg

3
@datUser โชคไม่ดี readarrayรุ่นของทุบตีต้องแก่เกินไปสำหรับ readในกรณีนี้คุณสามารถใช้วิธีการแก้ปัญหาสองที่ดีที่สุดที่สร้างขึ้นบน ฉันหมายถึงสิ่งนี้: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(ด้วยการawkทดแทนหากคุณต้องการการสนับสนุนตัวคั่นหลายตัว) แจ้งให้เราทราบหากคุณพบปัญหาใด ๆ ฉันค่อนข้างแน่ใจว่าโซลูชันนี้ควรใช้กับทุบตีเวอร์ชันเก่าค่อนข้างกลับไปเป็นรุ่นที่ 2 ซึ่งมีวางจำหน่ายเหมือนสองทศวรรษที่ผ่านมา
bgoldst

1
ว้าวช่างเป็นคำตอบที่ยอดเยี่ยม ฮิฮิคำตอบของฉัน: เขียนสคริปต์ทุบตีและยิงงูหลามขึ้น!
artfulrobot

1
@datUser ทุบตีบน OSX ยังคงติดอยู่ที่ 3.2 (เผยแพร่แคลิฟอร์เนีย 2007); ฉันใช้ทุบตีที่พบใน Homebrew เพื่อรับเวอร์ชั่น 4.X bash ใน OS X
JDS

222

นี่เป็นวิธีที่ไม่มีการตั้งค่า IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

แนวคิดกำลังใช้การแทนที่สตริง:

${string//substring/replacement}

เพื่อแทนที่การจับคู่ทั้งหมดของ $ substring ด้วย white space แล้วใช้สตริงที่แทนที่เพื่อเริ่มต้นอาร์เรย์:

(element1 element2 ... elementN)

หมายเหตุ: คำตอบนี้ทำให้การใช้แยก + ผู้ประกอบการ ดังนั้นเพื่อป้องกันการขยายตัวของอักขระบางตัว (เช่น*) มันเป็นความคิดที่ดีที่จะหยุด globbing สำหรับสคริปต์นี้ชั่วคราว


1
ใช้วิธีนี้ ... จนกว่าฉันจะเจอสตริงยาว ๆ ที่จะแยก CPU 100% นานกว่าหนึ่งนาที (จากนั้นฉันก็ฆ่ามัน) มันเป็นเรื่องที่น่าเสียดายเพราะวิธีการนี้อนุญาตให้แยกโดยสตริงไม่ใช่อักขระบางตัวใน IFS
เวอร์เนอร์เลห์มันน์

เวลา CPU 100% เป็นเวลาหนึ่งนาทีฟังฉันเหมือนจะต้องมีบางอย่างผิดปกติ สตริงนั้นยาวแค่ไหนมันเป็นขนาด MB หรือ GB? ฉันคิดว่าโดยปกติถ้าคุณต้องการแยกสตริงเล็ก ๆ คุณต้องอยู่ใน Bash แต่ถ้ามันเป็นไฟล์ขนาดใหญ่

12
คำเตือน: เพิ่งพบปัญหาเกี่ยวกับวิธีการนี้ หากคุณมีองค์ประกอบชื่อ * คุณจะได้รับองค์ประกอบทั้งหมดของ cwd ของคุณเช่นกัน string = "1: 2: 3: 4: *" จะให้ผลลัพธ์ที่ไม่คาดคิดและอาจเป็นอันตรายขึ้นอยู่กับการใช้งานของคุณ ไม่ได้รับข้อผิดพลาดเดียวกันกับ (IFS = ',' read -a array <<< "$ string") และอันนี้ดูเหมือนว่าจะปลอดภัย
Dieter Gribnitz

4
การ${string//:/ }ป้องกันการขยายตัวของเชลล์
แอนดรูว์ไวท์

1
ฉันต้องใช้สิ่งต่อไปนี้บน OSX: array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

พิมพ์สาม


8
ฉันชอบวิธีนี้มากกว่า ง่าย
shrimpwagon

4
ฉันคัดลอกและวางสิ่งนี้และมันไม่ได้ทำงานกับเสียงสะท้อน แต่ได้ผลเมื่อฉันใช้มันเพื่อการวนซ้ำ
Ben

2
สิ่งนี้ไม่ทำงานตามที่ระบุไว้ @ Jmoney38 หรือ shrimpwagon หากคุณสามารถวางสิ่งนี้ในเทอร์มินัลและรับเอาท์พุทที่ต้องการกรุณาวางผลลัพธ์ที่นี่
หยุดงาน

2
@abalter a=($(echo $t | tr ',' "\n"))ทำงานให้ฉันด้วย a=($(echo $t | tr ',' ' '))ผลเช่นเดียวกันกับ
ใบไม้

@procrastinator ฉันพยายามในVERSION="16.04.2 LTS (Xenial Xerus)"ในbashเปลือกและสุดท้ายechoเพียงแค่พิมพ์บรรทัดว่าง คุณใช้ Linux รุ่นใดและเชลล์รุ่นใดอยู่ น่าเสียดายที่ไม่สามารถแสดงเซสชันเทอร์มินัลในความคิดเห็นได้
ปิดบัง

29

บางครั้งมันก็เกิดขึ้นกับฉันว่าวิธีการที่อธิบายไว้ในคำตอบที่ยอมรับไม่ได้โดยเฉพาะอย่างยิ่งถ้าตัวแยกเป็นผลตอบแทนการขนส่ง
ในกรณีเหล่านั้นฉันแก้ไขด้วยวิธีนี้:

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"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 นี่ทำงานได้สมบูรณ์สำหรับฉัน ฉันต้องใส่สตริงหลาย ๆ ตัวหารด้วยการขึ้นบรรทัดใหม่เข้าไปในอาร์เรย์และread -a arr <<< "$strings"ไม่สามารถใช้งานIFS=$'\n'ได้
Stefan van den Akker

4
นี่คือคำตอบที่จะทำให้การทำงานที่ตอบรับเมื่อคั่นเป็นบรรทัดใหม่
Stefan van den Akker

สิ่งนี้ไม่ตอบคำถามเดิมมากนัก
Mike

29

คำตอบที่ยอมรับนั้นใช้ได้กับค่าในหนึ่งบรรทัด
หากตัวแปรมีหลายบรรทัด:

string='first line
        second line
        third line'

เราต้องการคำสั่งที่แตกต่างกันมากในการรับทุกบรรทัด:

while read -r line; do lines+=("$line"); done <<<"$string"

หรือreadarrayทุบตีง่ายกว่ามาก :

readarray -t lines <<<"$string"

การพิมพ์ทุกบรรทัดนั้นง่ายมากโดยใช้ประโยชน์จากคุณสมบัติ printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
ในขณะที่วิธีการแก้ปัญหาไม่ได้ทำงานได้ในทุกสถานการณ์การพูดถึงการอ่านของคุณ ... แทนที่สองชั่วโมงสุดท้ายของฉันด้วย 5 นาที ... คุณได้รับคะแนนของฉัน
Angry 84


6

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

หากคุณกำหนดให้IFS=", "สตริงจะแตกบน EITHER ","หรือ" "หรือการรวมกันของพวกเขาซึ่งไม่ได้เป็นตัวแทนที่ถูกต้องของตัวละครทั้งสองตัวคั่นของ", "หรือการรวมกันของพวกเขาซึ่งไม่ได้เป็นตัวแทนที่ถูกต้องของตัวคั่นสองตัวละคร

คุณสามารถใช้awkหรือsedแยกสตริงด้วยการทดแทนกระบวนการ:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

การใช้ regex ของคุณโดยตรงใน Bash นั้นมีประสิทธิภาพมากกว่า

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

ด้วยรูปแบบที่สองไม่มีเชลล์ย่อยและมันจะเร็วขึ้นโดยเนื้อแท้


แก้ไขโดย bgoldst:นี่คือมาตรฐานเปรียบเทียบreadarrayโซลูชันของฉันกับโซลูชัน regex ของ dawg และฉันยังรวมreadโซลูชันสำหรับ heck ของมัน (หมายเหตุ: ฉันปรับเปลี่ยนโซลูชัน regex เล็กน้อยเพื่อให้สอดคล้องกับโซลูชันของฉันมากขึ้น) (ดูความคิดเห็นของฉันด้านล่าง โพสต์)

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

ทางออกที่ยอดเยี่ยมมาก! ฉันไม่เคยคิดว่าการใช้ห่วงในการแข่งขัน regex $BASH_REMATCHใช้ที่ดีของ มันใช้งานได้และหลีกเลี่ยงการวางไข่ subshells แน่นอน +1 จากฉัน อย่างไรก็ตามตามคำวิจารณ์ Regex เองก็ไม่เหมาะอย่างยิ่งในกรณีที่คุณถูกบังคับให้ทำซ้ำส่วนหนึ่งของโทเค็นตัวคั่น (โดยเฉพาะเครื่องหมายจุลภาค) เพื่อหลีกเลี่ยงการสนับสนุนตัวคูณที่ไม่โลภ (lookarounds) ใน ERE ("ขยาย" รสชาติ regex ที่สร้างขึ้นในทุบตี) สิ่งนี้ทำให้สามัญน้อยลงและแข็งแกร่งขึ้นเล็กน้อย
bgoldst

ประการที่สองฉันทำการเปรียบเทียบและแม้ว่าประสิทธิภาพจะดีกว่าโซลูชันอื่น ๆ สำหรับสตริงขนาดเล็ก แต่ก็แย่ลงอย่างมากเนื่องจากการสร้างสตริงซ้ำซ้ำแล้วซ้ำอีกกลายเป็นหายนะสำหรับสตริงที่มีขนาดใหญ่มาก ดูการแก้ไขคำตอบของฉัน
bgoldst

@bgoldst: ช่างเป็นมาตรฐานที่ยอดเยี่ยม! ในการป้องกัน regex สำหรับเขตข้อมูล 10 หรือ 100 ของพัน (สิ่งที่ regex จะแบ่ง) อาจจะมีรูปแบบของการบันทึกบางอย่าง (เช่น\nบรรทัดข้อความที่คั่นด้วย) ประกอบด้วยเขตข้อมูลเหล่านั้นดังนั้นการชะลอตัวลงความหายนะจะไม่เกิดขึ้น หากคุณมีสตริงที่มี 100,000 ฟิลด์ - บางที Bash อาจไม่เหมาะ ;-) ขอขอบคุณสำหรับการวัดประสิทธิภาพ ฉันเรียนรู้สิ่งหนึ่งหรือสองอย่าง
dawg

4

วิธีการแก้ปัญหาตัวคั่นหลายตัวอักษรทุบตีบริสุทธิ์

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

เนื่องจาก Google มีแนวโน้มที่จะจัดอันดับคำตอบนี้ที่หรือใกล้ด้านบนของผลการค้นหาฉันต้องการให้ผู้อ่านมีคำตอบที่ดีสำหรับคำถามเกี่ยวกับตัวคั่นหลายตัวเนื่องจากเป็นที่กล่าวถึงอย่างน้อยหนึ่งคำตอบ

หากคุณกำลังค้นหาวิธีแก้ไขปัญหาตัวคั่นหลายตัวฉันขอแนะนำให้ตรวจสอบโพสต์ของMallikarjun Mโดยเฉพาะคำตอบจากgniourf_gniourf ซึ่งให้โซลูชัน BASH บริสุทธิ์ที่หรูหรานี้โดยใช้การขยายพารามิเตอร์:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

ลิงก์ไปยังอ้างถึงความคิดเห็น / อ้างอิงโพสต์

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


1
ดูความคิดเห็นของฉันสำหรับวิธีที่คล้ายกัน แต่ปรับปรุงแล้ว
xebeche

3

สิ่งนี้ใช้ได้กับฉันใน OSX:

string="1 2 3 4 5"
declare -a array=($string)

หากสตริงของคุณมีตัวคั่นต่างกันเพียงอันดับแรกให้แทนที่ด้วยช่องว่าง:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

ง่าย :-)


ทำงานได้ทั้ง Bash และ Zsh ซึ่งเป็นข้อดี!
Elijah W. Gagne

2

อีกวิธีในการทำโดยไม่แก้ไข IFS:

read -r -a myarray <<< "${string//, /$IFS}"

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

บางทีนี่อาจจะช้าสำหรับสตริงที่มีขนาดใหญ่มากใช่ไหม

นี่คือคำตอบของ Dennis Williamson


2

ฉันเจอโพสต์นี้เมื่อต้องการแยกวิเคราะห์อินพุตเช่น: word1, word2, ...

ไม่มีข้อใดช่วยฉันได้ แก้ไขได้โดยใช้ awk ถ้าช่วยคน:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

ลองสิ่งนี้

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

มันง่ายมาก หากคุณต้องการคุณสามารถเพิ่มประกาศ (และลบเครื่องหมายจุลภาค):

IFS=' ';declare -a array=(Paris France Europe)

IFS ถูกเพิ่มเพื่อเลิกทำข้างต้น แต่มันทำงานได้โดยไม่ต้องมันในอินสแตนซ์ทุบตีใหม่


1

เราสามารถใช้คำสั่ง tr เพื่อแยกสตริงเป็นวัตถุอาร์เรย์ มันทำงานได้ทั้ง MacOS และ Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

ตัวเลือกอื่นใช้คำสั่ง IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

ใช้สิ่งนี้:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
ไม่ดี: ขึ้นอยู่กับการแยกคำและการขยายชื่อพา ธ โปรดอย่ารื้อฟื้นคำถามเก่า ๆ พร้อมคำตอบที่ดีเพื่อให้คำตอบที่ไม่ดี
gniourf_gniourf

2
นี่อาจเป็นคำตอบที่ไม่ดี แต่ก็ยังเป็นคำตอบที่ถูกต้อง ผู้ตั้งค่าสถานะ / ผู้ตรวจสอบ: สำหรับคำตอบที่ไม่ถูกต้องเช่นคำนี้ลงคะแนนอย่าลบ!
Scott Weldon

2
@gniourf_gniourf คุณช่วยอธิบายได้ไหมว่าทำไมมันถึงเป็นคำตอบที่ไม่ดี? ฉันไม่เข้าใจจริงๆเมื่อมันล้มเหลว
George Sovetov

3
@GeorgeSovetov: อย่างที่ฉันบอกไปมันอาจมีการแยกคำและขยายชื่อพา ธ โดยทั่วไปแล้วการแบ่งสตริงลงในอาร์เรย์เหมือนarray=( $string )antipattern (ที่น่าเศร้ามาก): การแบ่งคำเกิดขึ้น: string='Prague, Czech Republic, Europe'; การขยายชื่อพา ธ เกิดขึ้น: string='foo[abcd],bar[efgh]'จะล้มเหลวหากคุณมีไฟล์ชื่อเช่นfoodหรือbarfในไดเรกทอรีของคุณ การใช้งานที่ถูกต้องเพียงอย่างเดียวของโครงสร้างนี้คือเมื่อใดstringคือ glob
gniourf_gniourf

0

UPDATE: อย่าทำเช่นนี้เนื่องจากปัญหาเกี่ยวกับ eval

ด้วยพิธีน้อยลงเล็กน้อย:

IFS=', ' eval 'array=($string)'

เช่น

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
eval เป็นความชั่วร้าย! อย่าทำอย่างนี้
caesarsol

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

2
ใส่$ในตัวแปรของคุณและคุณจะเห็น ... ฉันเขียนสคริปต์มากมายและฉันไม่เคยต้องใช้ซิงเกิ้ลeval
caesarsol

2
คุณพูดถูกสิ่งนี้สามารถใช้งานได้ก็ต่อเมื่อข้อมูลที่ป้อนเข้านั้นสะอาด ไม่ใช่ทางออกที่แข็งแกร่ง
user1009908

ครั้งเดียวที่ฉันต้องใช้ eval สำหรับแอปพลิเคชั่นที่จะสร้างรหัส / โมดูลของตัวเอง ... และสิ่งนี้ไม่เคยมีรูปแบบการป้อนข้อมูลของผู้ใช้ ...
Angry 84

0

นี่คือแฮ็คของฉัน!

การแยกสตริงด้วยสตริงเป็นสิ่งที่น่าเบื่อเมื่อใช้ bash สิ่งที่เกิดขึ้นคือเรามีวิธีการ จำกัด ที่ใช้งานได้ในบางกรณีเท่านั้น (แยกตาม ";", "/", "." และอื่น ๆ ) หรือเรามีผลข้างเคียงที่หลากหลายในผลลัพธ์

วิธีการด้านล่างนี้ต้องใช้การซ้อมรบหลายครั้ง แต่ฉันเชื่อว่ามันจะทำงานได้ตามความต้องการของเรา!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

สำหรับองค์ประกอบหลายรายการทำไมไม่ชอบ

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

อีกวิธีคือ:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

ตอนนี้องค์ประกอบของคุณจะถูกเก็บไว้ในอาร์เรย์ "arr" ในการวนซ้ำองค์ประกอบต่างๆ:

for i in ${arr[@]}; do echo $i; done

1
ฉันครอบคลุมความคิดนี้ในคำตอบของฉัน ; ดูคำตอบที่ผิด # 5 (คุณอาจสนใจโดยเฉพาะในการอภิปรายของฉันevalเคล็ดลับ) วิธีแก้ปัญหาของคุณปล่อย$IFSให้ตั้งค่าเป็นจุลภาคหลังจากความจริง
bgoldst

-1

เนื่องจากมีหลายวิธีในการแก้ไขปัญหานี้เริ่มต้นด้วยการกำหนดสิ่งที่เราต้องการเห็นในโซลูชันของเรา

  1. Bash จัดทำบิวด์อินreadarrayสำหรับจุดประสงค์นี้ มาใช้กันเถอะ
  2. หลีกเลี่ยงเทคนิคที่น่าเกลียดและไม่จำเป็นเช่นการเปลี่ยนการIFSวนซ้ำการใช้evalหรือการเพิ่มองค์ประกอบพิเศษจากนั้นลบออก
  3. ค้นหาวิธีที่ง่ายและอ่านได้ซึ่งสามารถปรับให้เข้ากับปัญหาที่คล้ายกันได้อย่างง่ายดาย

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

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

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

วิธีการอื่นสามารถ:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

หลังจากนี้ 'arr' เป็นอาร์เรย์ที่มีสี่สาย สิ่งนี้ไม่ต้องการการจัดการ IFS หรืออ่านหรือสิ่งพิเศษอื่น ๆ ดังนั้นจึงง่ายและตรงไปตรงมาก


antipattern เดียวกัน (ที่น่าเศร้าทั่วไป) เช่นเดียวกับคำตอบอื่น ๆ : ขึ้นอยู่กับการแยกคำและการขยายชื่อไฟล์
gniourf_gniourf
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.