ใน bash script วิธีจับ stdout ทีละบรรทัด


26

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

# Start long command in a separated process and redirect stdout to temp file
longcommand > /tmp/tmp$$.out &

#loop until process completes
ps cax | grep longcommand > /dev/null
while [ $? -eq 0 ]
do
    #capture the last lines in temp file and determine if there is new content to analyse
    tail /tmp/tmp$$.out

    # ...

    sleep 1 s  # sleep in order not to clog cpu

    ps cax | grep longcommand > /dev/null
done

ฉันต้องการทราบว่ามีวิธีที่ง่ายกว่าในการทำเช่นนั้น

แก้ไข:

เพื่อชี้แจงคำถามของฉันฉันจะเพิ่มนี้ longcommandแสดงบรรทัดสถานะของตนโดยสายหนึ่งครั้งต่อวินาที ฉันต้องการที่จะจับเอาท์พุทก่อนที่จะlongcommandเสร็จสมบูรณ์

ด้วยวิธีนี้ฉันสามารถฆ่าlongcommandถ้ามันไม่ได้ให้ผลลัพธ์ที่ฉันคาดหวัง

ฉันเหนื่อย:

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

แต่whatever(เช่นecho) จะดำเนินการหลังจากlongcommandเสร็จสิ้นเท่านั้น


คำตอบ:


32

เพียงไพพ์คำสั่งลงในwhileลูป มีจำนวนของความแตกต่างนี้ แต่โดยทั่วไป (ในbashหรือเปลือก POSIX ใด ๆ ):

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

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

longcommand | {
  while IFS= read -r line
  do
    whatever "$line"
    lastline="$line"
  done

  # This won't work without the braces.
  echo "The last line was: $lastline"
}

ตัวอย่าง Hauke ของการตั้งค่าlastpipeในการbashเป็นวิธีการแก้อีก

ปรับปรุง

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

stdbuf -oL longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

สิ่งนี้จะกำหนดค่ากระบวนการเพื่อเขียนทีละหนึ่งบรรทัดในไพพ์แทนการบัฟเฟอร์ภายในเอาต์พุตลงในบล็อก ระวังว่าโปรแกรมสามารถเปลี่ยนการตั้งค่านี้เองภายใน ผลที่คล้ายกันสามารถทำได้ด้วยunbuffer(ส่วนหนึ่งของexpect) scriptหรือ

stdbufพร้อมใช้งานบนระบบ GNU และ FreeBSD ซึ่งมีผลกับstdioบัฟเฟอร์เท่านั้นและใช้ได้กับแอปพลิเคชันที่ไม่ใช่ setuid และไม่ใช่ setgid ที่เชื่อมโยงแบบไดนามิก (เนื่องจากใช้เคล็ดลับ LD_PRELOAD)


@Stephane The IFS=ไม่จำเป็นในbashฉันได้ตรวจสอบเรื่องนี้หลังจากครั้งที่แล้ว
แกรม

2
ใช่แล้ว. ไม่จำเป็นถ้าคุณไม่ใช้line(ในกรณีนี้ผลลัพธ์จะถูกใส่$REPLYโดยไม่มีการเว้นวรรคนำหน้าและต่อท้าย) ลอง: echo ' x ' | bash -c 'read line; echo "[$line]"'และเปรียบเทียบกับecho ' x ' | bash -c 'IFS= read line; echo "[$line]"'หรือecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas

@ สเตฟานโอเคไม่เคยรู้เลยว่ามีความแตกต่างระหว่างสิ่งนั้นกับตัวแปรที่ตั้งชื่อ ขอบคุณ
แกรม

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

@gfrigon อัปเดตแล้ว
แกรม

2
#! /bin/bash
set +m # disable job control in order to allow lastpipe
shopt -s lastpipe
longcommand |
  while IFS= read -r line; do lines[i]="$line"; ((i++)); done
echo "${lines[1]}" # second line
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.