วิธีการแยกหนึ่งสายออกเป็นหลายสายโดยคั่นด้วยช่องว่างอย่างน้อยหนึ่งช่องในเปลือกหอย


224

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

สตริงถูกส่งเป็นอาร์กิวเมนต์ ${2} == "cat cat file"เช่น ฉันจะวนซ้ำมันได้อย่างไร

นอกจากนี้ฉันจะตรวจสอบว่าสตริงมีช่องว่างได้อย่างไร


1
เปลือกชนิดใด Bash, cmd.exe, powershell ... ?
Alexey Sviridov

คุณเพียงแค่ต้องห่วง (เช่นรันคำสั่งสำหรับแต่ละคำ)? หรือคุณต้องการจัดเก็บรายการคำเพื่อใช้ในภายหลัง
DVK

คำตอบ:


281

คุณลองส่งผ่านตัวแปรสตริงไปยังforลูปหรือไม่ Bash สำหรับหนึ่งจะแยกบนช่องว่างโดยอัตโนมัติ

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.

1
@MobRule - ข้อเสียเปรียบเพียงอย่างเดียวคือคุณไม่สามารถจับภาพได้อย่างง่ายดาย (อย่างน้อยฉันก็ไม่สามารถจำได้) เอาท์พุทสำหรับการประมวลผลเพิ่มเติม ดูวิธีแก้ปัญหา "tr" ด้านล่างสำหรับสิ่งที่ส่งไปยัง STDOUT
DVK

4
A=${A}${word})คุณก็สามารถผนวกกับตัวแปร:
Lucas Jones

1
$ ข้อความชุด [นี้จะใส่ถ้อยคำลง $ 1, $ 2, $ 3 ... ฯลฯ ]
Rajesh

32
อันที่จริงเคล็ดลับนี้ไม่เพียง แต่เป็นวิธีแก้ปัญหาที่ผิด แต่ยังเป็นอันตรายอย่างยิ่งเนื่องจากเปลือกกลม touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; doneเอาท์พุท[NOPE] [a] [NOPE]แทนที่จะเป็นที่คาดหวัง[*] [a] [*](LFs แทนที่ด้วย SPC เพื่อให้สามารถอ่านได้)
Tino

@mob ฉันควรทำอย่างไรหากฉันต้องการแยกสตริงตามสตริงเฉพาะบางรายการ ตัวอย่างเช่น".xlsx"คั่น

296

ฉันชอบการแปลงเป็นอาเรย์เพื่อให้สามารถเข้าถึงแต่ละองค์ประกอบ:

sentence="this is a story"
stringarray=($sentence)

ตอนนี้คุณสามารถเข้าถึงแต่ละองค์ประกอบได้โดยตรง (เริ่มต้นด้วย 0):

echo ${stringarray[0]}

หรือแปลงกลับเป็นสตริงเพื่อวนซ้ำ:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

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

for i in $sentence
do
  :
  # do whatever on $i
done

ดูเพิ่มเติมทุบตีอาร์เรย์อ้างอิง


26
น่าเศร้าที่ค่อนข้างไม่สมบูรณ์แบบเนื่องจากเปลือกหอยกลม: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=เอาท์พุทarr=([0]="NOPE" [1]="a" [2]="NOPE")แทนที่คาดไว้arr=([0]="*" [1]="a" [2]="*")
Tino

@Tino: หากคุณไม่ต้องการให้ globbing เข้าไปยุ่งก็แค่ปิด การแก้ปัญหาจะทำงานได้ดีกับสัญลักษณ์แทนเช่นกัน มันเป็นวิธีการที่ดีที่สุดในความคิดของฉัน
Alexandros

3
@Alexandros แนวทางของฉันคือใช้รูปแบบเท่านั้นซึ่งปลอดภัยโดยค่าเริ่มต้นและทำงานในทุกบริบทอย่างสมบูรณ์ ความต้องการที่จะเปลี่ยนเปลือกกลมเพื่อให้ได้ทางออกที่ปลอดภัยนั้นเป็นมากกว่าเพียงแค่เส้นทางที่อันตรายมากมันเป็นด้านมืดแล้ว ดังนั้นคำแนะนำของฉันคือไม่เคยคุ้นเคยกับการใช้รูปแบบเช่นนี้ที่นี่เพราะไม่ช้าก็เร็วคุณจะลืมรายละเอียดบางอย่างจากนั้นก็มีคนใช้ประโยชน์จากข้อบกพร่องของคุณ คุณสามารถหาหลักฐานการหาประโยชน์ดังกล่าวได้ในสื่อ ทุกๆ เดียว วัน.
Tino

86

เพียงใช้เชลล์ในตัว "ชุด" ในตัว ตัวอย่างเช่น,

กำหนด $ text

หลังจากนั้นคำแต่ละคำใน $ text จะเป็น $ 1, $ 2, $ 3 และอื่น ๆ เพื่อความแข็งแรงมักจะมี

ชุด - ข้อความขยะ $
เปลี่ยน

เพื่อจัดการกรณีที่ $ text ว่างเปล่าหรือเริ่มต้นด้วยเส้นประ ตัวอย่างเช่น:

text = "นี่คือการทดสอบ"
ชุด - ข้อความขยะ $
เปลี่ยน
สำหรับคำ; ทำ
  echo "[$ word]"
เสร็จแล้ว

ภาพพิมพ์นี้

[นี้]
[คือ]
[เป็น]
[ทดสอบ]

5
นี่เป็นวิธีที่ยอดเยี่ยมในการแยก var เพื่อให้แต่ละส่วนสามารถเข้าถึงได้โดยตรง +1; แก้ไขปัญหาของฉัน
Cheekysoft

ฉันจะแนะนำให้ใช้awkแต่setง่ายกว่ามาก ตอนนี้ฉันเป็นsetแฟนบอย ขอบคุณ @Idelic!
Yzmir Ramirez

22
โปรดระวังเปลือก globbing ถ้าคุณทำสิ่งต่างๆเช่น: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; doneผลแทนการที่คาดว่าจะ [NOPE] [a] [NOPE] ใช้งานได้เฉพาะในกรณีที่คุณแน่ใจว่า 101% แน่ใจว่าไม่มีอักขระเมเซล SHELL ในสตริงที่แยก! [*] [a] [*]
Tino

4
@ ติโน่: ปัญหานั้นเกิดขึ้นได้ทุกที่ไม่เพียง แต่ที่นี่ แต่ในกรณีนี้คุณสามารถปิดใช้งานset -fก่อนset -- $varและset +fหลังได้
Idelic

3
@Idelic: จับได้ดี ด้วยset -fโซลูชันของคุณก็ปลอดภัยเช่นกัน แต่set +fเป็นค่าเริ่มต้นของแต่ละเชลล์ดังนั้นจึงเป็นรายละเอียดที่สำคัญซึ่งจะต้องมีการบันทึกไว้เพราะคนอื่น ๆ อาจไม่ได้ตระหนักถึงมัน (เหมือนที่ฉันเคยเป็น)
Tino

81

วิธีที่ง่ายและปลอดภัยที่สุดใน BASH 3 ขึ้นไปคือ:

var="string    to  split"
read -ra arr <<<"$var"

(ซึ่งarrเป็นอาร์เรย์ที่รับส่วนที่แยกของสตริง) หรือหากอาจมีการขึ้นบรรทัดใหม่ในอินพุตและคุณต้องการมากกว่าแค่บรรทัดแรก:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(โปรดทราบว่าพื้นที่ใน-d ''นั้นไม่สามารถถูกทิ้งไว้ได้) แต่สิ่งนี้อาจทำให้คุณได้รับ newline ที่ไม่คาดคิดจาก<<<"$var"(เนื่องจากเป็นการเพิ่ม LF ในตอนท้าย)

ตัวอย่าง:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

แสดงผลที่คาดหวัง

[*]
[a]
[*]

เนื่องจากโซลูชันนี้ (ตรงกันข้ามกับโซลูชันก่อนหน้าทั้งหมดที่นี่) จึงไม่น่าจะเกิดความคาดไม่ถึงและมักจะไม่สามารถควบคุมเชลล์ได้

สิ่งนี้จะช่วยให้คุณมีพลังอย่างเต็มที่ของ IFS อย่างที่คุณต้องการ:

ตัวอย่าง:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

เอาต์พุตคล้าย:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

อย่างที่คุณเห็นช่องว่างสามารถรักษาด้วยวิธีนี้ได้เช่นกัน:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

เอาท์พุท

[ split  ]
[   this    ]

โปรดทราบว่าการจัดการIFSใน BASH นั้นเป็นหัวข้อของตัวเองดังนั้นการทดสอบของคุณจึงมีหัวข้อที่น่าสนใจเกี่ยวกับเรื่องนี้:

  • unset IFS: ละเว้นการทำงานของ SPC, TAB, NL และบนบรรทัดเริ่มต้นและสิ้นสุด
  • IFS='': ไม่ต้องแยกฟิลด์อ่านทุกอย่าง
  • IFS=' ': Runs ของ SPC (และ SPC เท่านั้น)

ตัวอย่างสุดท้าย

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

เอาท์พุท

1 [this is]
2 [a test]

ในขณะที่

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

เอาท์พุท

1 [this]
2 [is]
3 [a]
4 [test]

BTW:

  • หากคุณไม่คุ้นเคยกับการ$'ANSI-ESCAPED-STRING'ใช้มันก็เป็นไทม์เมอร์

  • หากคุณไม่รวม-r(เหมือนในread -a arr <<<"$var") การอ่านจะมีเครื่องหมายแบ็กสแลชหนี นี่เป็นแบบฝึกหัดสำหรับผู้อ่าน


สำหรับคำถามที่สอง:

เพื่อทดสอบบางสิ่งบางอย่างในสตริงฉันมักจะติดcaseเช่นนี้สามารถตรวจสอบหลายกรณีพร้อมกัน (หมายเหตุ: กรณีเท่านั้นดำเนินการจับคู่แรกถ้าคุณต้องการใช้caseคำสั่งที่ใช้หลายข้อผิดพลาด) และความต้องการนี้ค่อนข้างบ่อย เจตนา):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

ดังนั้นคุณสามารถตั้งค่าส่งคืนเพื่อตรวจสอบ SPC ดังนี้:

case "$var" in (*' '*) true;; (*) false;; esac

ทำไมcase? เนื่องจากโดยปกติแล้วจะสามารถอ่านได้ง่ายกว่าลำดับของ regex และต้องขอบคุณ Shell Metacharacters ที่สามารถจัดการได้ 99% ของความต้องการทั้งหมดเป็นอย่างดี


2
คำตอบนี้สมควรได้รับการโหวตมากขึ้นเนื่องจากประเด็นที่เน้นความสำคัญและความครอบคลุม
Brian Agnew

@Brian ขอบคุณ โปรดทราบว่าคุณสามารถใช้set -fหรือset -o noglobเพื่อเปลี่ยนการวนรอบได้เช่นกันว่าอักขระเมตาเชลล์จะไม่เป็นอันตรายต่อบริบทนี้อีกต่อไป แต่ฉันไม่ได้เป็นเพื่อนของสิ่งนี้เพราะมันทิ้งพลังไว้มากมายของเปลือกหอย / เป็นข้อผิดพลาดอย่างมากที่จะเปลี่ยนการตั้งค่านี้กลับไปกลับมา
Tino

2
คำตอบที่ยอดเยี่ยมแน่นอนสมควรได้รับการโหวตมากขึ้น หมายเหตุด้านข้างเกี่ยวกับกรณีที่ล้มเหลว - คุณสามารถใช้;&ให้บรรลุเป้าหมายนั้นได้ ไม่แน่ใจใน bash รุ่นที่ปรากฏ ฉันเป็นผู้ใช้ 4.3
Sergiy Kolodyazhnyy

2
@Serg ขอบคุณที่สังเกตเพราะฉันยังไม่รู้สิ่งนี้! ดังนั้นผมมองว่ามันขึ้นก็ปรากฏตัวขึ้นในBash4 ;&เป็นข้อผิดพลาดที่บังคับโดยไม่มีการตรวจสอบรูปแบบเหมือนใน C และนอกจากนี้ยังมี;;&เพียงที่จะทำการตรวจสอบรูปแบบเพิ่มเติม ดังนั้น;;เป็นเหมือนif ..; then ..; else if ..และ;;&เป็นเหมือนif ..; then ..; fi; if ..ที่;&เป็นเหมือนm=false; if ..; then ..; m=:; fi; if $m || ..; then ..- หนึ่งไม่เคยหยุดการเรียนรู้ (จากคนอื่น ๆ );)
โน่

@Tino นั่นจริงทั้งหมด - การเรียนรู้เป็นกระบวนการต่อเนื่อง ในความเป็นจริงฉันไม่ทราบ ;;&ก่อนที่คุณจะแสดงความคิดเห็น: D ขอบคุณและอาจเปลือกอยู่กับคุณ;)
Sergiy Kolodyazhnyy

43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

สำหรับการตรวจสอบช่องว่างให้ใช้ grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1

1
ในทุบตีecho "X" |สามารถมักจะถูกแทนที่ด้วยเช่นนี้<<<"X" grep -s " " <<<"This contains SPC"คุณสามารถมองเห็นความแตกต่างถ้าคุณทำสิ่งที่ชอบในทางตรงกันข้ามกับecho X | read var read var <<< Xเฉพาะตัวแปรหลังที่นำเข้าvarมาในเชลล์ปัจจุบันในขณะที่เข้าถึงมันในตัวแปรแรกคุณต้องจัดกลุ่มดังนี้:echo X | { read var; handle "$var"; }
Tino

17

(A)ในการแบ่งประโยคเป็นคำ (คั่นด้วยช่องว่าง) คุณสามารถใช้ IFS เริ่มต้นโดยใช้

array=( $string )


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

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

จะส่งออก

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

อย่างที่คุณเห็นคุณสามารถใช้อัญประกาศเดี่ยวหรือคู่ได้โดยไม่มีปัญหา

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


(B)ในการตรวจสอบอักขระในสตริงคุณยังสามารถใช้การจับคู่นิพจน์ทั่วไป
ตัวอย่างเพื่อตรวจสอบว่ามีอักขระช่องว่างที่คุณสามารถใช้ได้:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi

สำหรับคำใบ้ regex (B) a +1 แต่ -1 สำหรับวิธีแก้ปัญหาที่ผิด (A) เนื่องจากนี่เป็นข้อผิดพลาดที่ทำให้เกิดการโค้งของเปลือก ;)
Tino


1
echo $WORDS | xargs -n1 echo

เอาต์พุตนี้ทุกคำคุณสามารถประมวลผลรายการนั้นตามที่เห็นสมควร

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