ฉันจะอ่านทีละบรรทัดจากตัวแปรใน bash ได้อย่างไร


48

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

ตัวอย่างเช่น:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi

คำตอบ:


64

คุณสามารถใช้วนลูปพร้อมกับการแทนที่กระบวนการ:

while read -r line
do
    echo "$line"
done < <(jobs)

วิธีที่ดีที่สุดในการอ่านตัวแปรหลายบรรทัดคือการตั้งค่าIFSตัวแปรว่างและprintfตัวแปรที่มีบรรทัดใหม่ต่อท้าย:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

หมายเหตุ: ตามshellcheck sc2031การใช้ substition ของกระบวนการจะดีกว่าไปป์เพื่อหลีกเลี่ยง [subtly] สร้าง subshell

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


3
ถ้าคุณต้องการรักษาพื้นที่ว่างทั้งหมดของคุณให้ใช้while IFS= read.... ถ้าคุณต้องการป้องกันการตีความ \ ให้ใช้read -r
Peter.O

ฉันได้แก้ไขคะแนน fred.bear ที่กล่าวถึงและเปลี่ยนเป็นechoเพื่อprintf %sให้สคริปต์ของคุณจะทำงานได้แม้กับอินพุตที่ไม่เชื่อง
Gilles 'หยุดความชั่วร้าย'

หากต้องการอ่านจากตัวแปรหลายบรรทัด herestring จะดีกว่าไปป์ไลน์จาก printf (ดูคำตอบของ l0b0)
ata

1
@ata ถึงแม้ว่าฉันเคยได้ยินเรื่องนี้ "ดีกว่า" บ่อยครั้ง แต่ก็ต้องมีข้อสังเกตว่า herestring นั้นต้องการ/tmpไดเรกทอรีที่สามารถเขียนได้เสมอเนื่องจากมันขึ้นอยู่กับความสามารถในการสร้างไฟล์งานชั่วคราว หากคุณเคยพบว่าตัวเองอยู่ในระบบที่ถูก จำกัด ด้วย/tmpการอ่านอย่างเดียว (และไม่สามารถเปลี่ยนแปลงได้โดยคุณ) คุณจะมีความสุขกับความเป็นไปได้ที่จะใช้โซลูชันทางเลือกเช่นกับprintfไปป์
ไวยากรณ์

1
ในตัวอย่างที่สองหากตัวแปรหลายบรรทัดไม่มีการขึ้นบรรทัดใหม่คุณจะเสียองค์ประกอบสุดท้าย เปลี่ยนเป็น:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett

26

ในการประมวลผลเอาต์พุตของบรรทัดคำสั่งทีละบรรทัด ( คำอธิบาย ):

jobs |
while IFS= read -r line; do
  process "$line"
done

หากคุณมีข้อมูลในตัวแปรอยู่แล้ว:

printf %s "$foo" | 

printf %s "$foo"เกือบจะเหมือนกันecho "$foo"แต่พิมพ์$fooตัวอักษรในขณะที่echo "$foo"อาจตีความ$fooเป็นตัวเลือกคำสั่ง echo ถ้าเริ่มต้นด้วย-และอาจขยายลำดับแบ็กสแลช$fooในเชลล์บางตัว

โปรดทราบว่าในเชลล์บางตัว (ash, bash, pdksh แต่ไม่ใช่ ksh หรือ zsh) ทางด้านขวาของไพพ์ไลน์จะทำงานในกระบวนการแยกต่างหากดังนั้นตัวแปรใด ๆ ที่คุณตั้งค่าในลูปจะหายไป ตัวอย่างเช่นสคริปต์นับบรรทัดต่อไปนี้พิมพ์ 0 ในเชลล์เหล่านี้:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

วิธีแก้ปัญหาคือการวางส่วนที่เหลือของสคริปต์ (หรืออย่างน้อยส่วนที่ต้องการค่า$nจากลูป) ในรายการคำสั่ง:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

หากการแสดงบนบรรทัดที่ไม่ว่างว่างนั้นดีพอและอินพุตไม่ใหญ่คุณสามารถใช้การแยกคำได้:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

คำอธิบาย: การตั้งค่าIFSเป็นหนึ่งบรรทัดใหม่ทำให้การแยกคำเกิดขึ้นที่บรรทัดใหม่เท่านั้น (ตรงข้ามกับอักขระช่องว่างใด ๆ ภายใต้การตั้งค่าเริ่มต้น) set -fจะปิด globbing (เช่นการขยายตัวสัญลักษณ์แทน) ซึ่งจะเกิดขึ้นกับผลของการทดแทนคำสั่ง$(jobs)หรือ $foosubstitutino forวงทำหน้าที่เกี่ยวกับชิ้นส่วนทั้งหมดของ$(jobs)ซึ่งเป็นทุกสายไม่ว่างเปล่าในการส่งออกคำสั่ง ในที่สุดคืนค่าการโค้งและIFSการตั้งค่า


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

1
@BjornRoche: local IFS=somethingภายในฟังก์ชั่นการใช้งาน มันจะไม่ส่งผลกระทบต่อค่าขอบเขตทั่วโลก IIRC unset IFSไม่นำคุณกลับไปสู่ค่าเริ่มต้น (และไม่สามารถใช้งานได้หากไม่ใช่ค่าเริ่มต้นล่วงหน้า)
Peter Cordes

14

ปัญหา: หากคุณใช้ในขณะที่ลูปมันจะทำงานใน subshell และตัวแปรทั้งหมดจะหายไป วิธีแก้ปัญหา: ใช้สำหรับลูป

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=

ขอบคุณมาก!! การแก้ปัญหาทั้งหมดข้างต้นล้มเหลวสำหรับฉัน
hax0r_n_code

การไพพ์ไปยังwhile readลูปใน bash หมายถึงขณะที่ลูปอยู่ใน subshell ดังนั้นตัวแปรจึงไม่ใช่โกลบอล while read;do ;done <<< "$var"ทำให้ร่างกายลูปไม่ใช่ subshell (ทุบตีล่าสุดมีตัวเลือกที่จะทำให้ร่างกายของการเป็นcmd | whileห่วงไม่ได้อยู่ใน subshell เช่น ksh ได้เคยมี.)
ปีเตอร์ Cordes


10

ในเวอร์ชัน bash ล่าสุดให้ใช้mapfileหรือreadarrayอ่านเอาต์พุตคำสั่งในอาร์เรย์อย่างมีประสิทธิภาพ

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

คำเตือน: ตัวอย่างที่น่ากลัว แต่คุณสามารถใช้คำสั่งที่ดีกว่าในการใช้งานได้มากกว่าคำสั่ง ls ด้วยตัวคุณเอง


เป็นวิธีที่ดี แต่ litters / var / tmp พร้อมไฟล์ชั่วคราวในระบบของฉัน +1 ต่อไป
Eugene Yarmash

@eugene: มันตลก ระบบอะไร (distro / OS) เปิดอยู่
sehe

มันเป็น FreeBSD 8. วิธีการทำซ้ำ: ใส่readarrayฟังก์ชั่นและเรียกใช้ฟังก์ชันสองสามครั้ง
Eugene Yarmash

นีซ @sehe +1
Teresa e Junior

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

อ้างอิง:


3
-rเป็นความคิดที่ดีเช่นกัน มันช่วยป้องกัน\` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(ซึ่งเป็นสิ่งสำคัญในการป้องกันการสูญเสียช่องว่าง)
Peter.O

-1

คุณสามารถใช้ <<< เพื่ออ่านจากตัวแปรที่มีข้อมูลที่คั่นด้วยบรรทัดใหม่:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"

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