ขณะนี้ฉันมีสคริปต์ที่ทำบางอย่างเช่น
./a | ./b | ./c
ฉันต้องการแก้ไขเพื่อที่ว่าถ้ามี a, b หรือ c ออกด้วยรหัสข้อผิดพลาดฉันพิมพ์ข้อความแสดงข้อผิดพลาดและหยุดแทนที่จะส่งผลลัพธ์ที่ไม่ดีไปข้างหน้า
อะไรคือวิธีที่ง่ายที่สุด / สะอาดที่สุดในการทำเช่นนั้น?
ขณะนี้ฉันมีสคริปต์ที่ทำบางอย่างเช่น
./a | ./b | ./c
ฉันต้องการแก้ไขเพื่อที่ว่าถ้ามี a, b หรือ c ออกด้วยรหัสข้อผิดพลาดฉันพิมพ์ข้อความแสดงข้อผิดพลาดและหยุดแทนที่จะส่งผลลัพธ์ที่ไม่ดีไปข้างหน้า
อะไรคือวิธีที่ง่ายที่สุด / สะอาดที่สุดในการทำเช่นนั้น?
a, b, cคำสั่งจะไม่ทำงานตามลำดับ แต่ในแบบคู่ขนาน ในคำอื่น ๆ กระแสข้อมูลตามลำดับจากaไปcแต่ที่เกิดขึ้นจริงa, bและcคำสั่งเริ่มต้น (ประมาณ) ในเวลาเดียวกัน
คำตอบ:
หากคุณไม่ต้องการให้คำสั่งที่สองดำเนินการต่อจนกว่าคำสั่งแรกจะสำเร็จคุณอาจต้องใช้ไฟล์ชั่วคราว รุ่นที่เรียบง่ายคือ:
tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
if ./b <$tmp.1 >$tmp.2
then
if ./c <$tmp.2
then : OK
else echo "./c failed" 1>&2
fi
else echo "./b failed" 1>&2
fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]
นอกจากนี้ยังสามารถย่อการเปลี่ยนเส้นทาง '1> & 2' '> & 2'; อย่างไรก็ตามเชลล์ MKS เวอร์ชันเก่าจัดการการเปลี่ยนเส้นทางข้อผิดพลาดโดยไม่ได้นำหน้า '1' ดังนั้นฉันจึงใช้สัญกรณ์ที่ชัดเจนเพื่อความน่าเชื่อถือสำหรับทุกวัย
ไฟล์นี้จะรั่วไหลหากคุณขัดจังหวะบางอย่าง การเขียนโปรแกรมเชลล์ป้องกันการระเบิด (มากหรือน้อย) ใช้:
tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15
บรรทัดกับดักแรกระบุว่า 'รันคำสั่ง' rm -f $tmp.[12]; exit 1'เมื่อสัญญาณใด ๆ 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE หรือ 15 SIGTERM เกิดขึ้นหรือ 0 (เมื่อเชลล์ออกด้วยเหตุผลใดก็ตาม) หากคุณกำลังเขียนเชลล์สคริปต์การดักจับขั้นสุดท้ายจะต้องลบกับดักที่ 0 เท่านั้นซึ่งเป็นกับดักทางออกของเชลล์ (คุณสามารถปล่อยสัญญาณอื่น ๆ ไว้ได้เนื่องจากกระบวนการกำลังจะยุติต่อไป)
ในไปป์ไลน์เดิมเป็นไปได้ที่ 'c' จะอ่านข้อมูลจาก 'b' ก่อนที่ 'a' จะเสร็จสิ้นซึ่งโดยปกติแล้วจะเป็นที่ต้องการ (เช่นให้หลายคอร์ทำงานเป็นต้น) ถ้า 'b' เป็นเฟส 'sort' ก็จะใช้ไม่ได้ - 'b' ต้องดูอินพุตทั้งหมดก่อนจึงจะสามารถสร้างเอาต์พุตใด ๆ ได้
หากคุณต้องการตรวจสอบว่าคำสั่งใดล้มเหลวคุณสามารถใช้:
(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)
นี่เป็นเรื่องง่ายและสมมาตร - เป็นเรื่องเล็กน้อยที่จะขยายไปยังท่อ 4 ส่วนหรือ N-part
การทดลองง่ายๆด้วย 'set -e' ไม่ได้ช่วยอะไร
mktempหรือtempfile.
trapโปรดทราบว่าผู้ใช้สามารถส่งSIGKILLไปยังกระบวนการเพื่อยุติกระบวนการได้ทันทีซึ่งในกรณีนี้กับดักของคุณจะไม่มีผล เช่นเดียวกันกับเมื่อระบบของคุณประสบปัญหาไฟฟ้าดับ เมื่อสร้างไฟล์ชั่วคราวอย่าลืมใช้mktempเพราะจะทำให้ไฟล์ของคุณอยู่ที่ไหนสักแห่งซึ่งจะถูกล้างหลังจากรีบูต (โดยปกติจะเป็น/tmp)
ในbashคุณสามารถใช้set -eและset -o pipefailที่จุดเริ่มต้นของไฟล์ของคุณ คำสั่งที่ตามมา./a | ./b | ./cจะล้มเหลวเมื่อสคริปต์ใด ๆ ในสามสคริปต์ล้มเหลว โค้ดส่งคืนจะเป็นโค้ดส่งคืนของสคริปต์แรกที่ล้มเหลว
โปรดทราบว่าpipefailไม่สามารถใช้ได้ในมาตรฐานการดวลจุดโทษ
set(รวม 4 เวอร์ชันที่แตกต่างกัน) และดูว่าสิ่งนั้นมีผลต่อผลลัพธ์ของเวอร์ชันล่าสุดechoอย่างไร
คุณยังสามารถตรวจสอบ${PIPESTATUS[]}อาร์เรย์หลังจากการดำเนินการเต็มรูปแบบเช่นถ้าคุณเรียกใช้:
./a | ./b | ./c
จากนั้น${PIPESTATUS}จะเป็นอาร์เรย์ของรหัสข้อผิดพลาดจากแต่ละคำสั่งในไปป์ดังนั้นหากคำสั่งกลางล้มเหลวecho ${PIPESTATUS[@]}จะมีสิ่งที่ต้องการ:
0 1 0
และสิ่งนี้จะทำงานหลังจากคำสั่ง:
test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0
จะช่วยให้คุณตรวจสอบว่าคำสั่งทั้งหมดในท่อสำเร็จ
#!/bin/shเพราะถ้าshไม่ทุบตีมันจะไม่ทำงาน (แก้ไขได้ง่ายโดยจำใช้#!/bin/bashแทน)
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- ส่งคืน 0 หาก$PIPESTATUS[@]มีเพียง 0 และช่องว่าง (หากคำสั่งทั้งหมดในไปป์สำเร็จ)
command1 && command2 | command3หากข้อใดล้มเหลวโซลูชันของคุณจะคืนค่าไม่เป็นศูนย์
น่าเสียดายที่คำตอบของ Johnathan ต้องการไฟล์ชั่วคราวและคำตอบของ Michel และ Imron ต้องใช้ bash (แม้ว่าคำถามนี้จะติดแท็กเชลล์ก็ตาม) ตามที่ผู้อื่นชี้ไว้แล้วว่าไม่สามารถยกเลิกท่อได้ก่อนที่กระบวนการในภายหลังจะเริ่มต้น กระบวนการทั้งหมดเริ่มต้นพร้อมกันและจะดำเนินการทั้งหมดก่อนที่จะมีการแจ้งข้อผิดพลาดใด ๆ แต่ชื่อคำถามก็ถามเกี่ยวกับรหัสข้อผิดพลาดด้วย สิ่งเหล่านี้สามารถเรียกดูและตรวจสอบได้หลังจากท่อเสร็จสิ้นเพื่อดูว่ากระบวนการที่เกี่ยวข้องล้มเหลวหรือไม่
นี่คือวิธีแก้ปัญหาที่ตรวจจับข้อผิดพลาดทั้งหมดในท่อและไม่ใช่เฉพาะข้อผิดพลาดของส่วนประกอบสุดท้ายเท่านั้น ดังนั้นนี้เป็นเหมือน pipefail ทุบตีของเพียงมีประสิทธิภาพมากขึ้นในแง่ที่ว่าคุณสามารถดึงทุกรหัสข้อผิดพลาด
res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
echo pipe failed
fi
เพื่อตรวจสอบว่าสิ่งใดล้มเหลวechoคำสั่งจะพิมพ์ข้อผิดพลาดมาตรฐานในกรณีที่คำสั่งใด ๆ ล้มเหลว จากนั้นเอาต์พุตข้อผิดพลาดมาตรฐานรวมจะถูกบันทึก$resและตรวจสอบในภายหลัง นี่คือสาเหตุที่ข้อผิดพลาดมาตรฐานของกระบวนการทั้งหมดถูกเปลี่ยนเส้นทางไปยังเอาต์พุตมาตรฐาน นอกจากนี้คุณยังสามารถส่งเอาต์พุตนั้นไปยัง/dev/nullหรือปล่อยไว้เป็นตัวบ่งชี้อื่นว่ามีบางอย่างผิดพลาด คุณสามารถแทนที่การเปลี่ยนเส้นทางสุดท้ายไปยัง/dev/nullไฟล์ได้หากคุณไม่ต้องการเก็บผลลัพธ์ของคำสั่งสุดท้ายไว้ที่ใดก็ได้
ในการเล่นมากขึ้นด้วยการสร้างนี้และที่จะโน้มน้าวตัวเองว่านี้จริงๆไม่สิ่งที่มันควรจะเป็นฉันแทนที่./a, ./bและ./cโดย subshells ซึ่งดำเนินการecho, และcat exitคุณสามารถใช้สิ่งนี้เพื่อตรวจสอบว่าโครงสร้างนี้ส่งต่อผลลัพธ์ทั้งหมดจากกระบวนการหนึ่งไปยังอีกกระบวนการหนึ่งจริง ๆ และรหัสข้อผิดพลาดได้รับการบันทึกอย่างถูกต้อง
res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
echo pipe failed
fi
&&|ซึ่งหมายความว่า "ดำเนินการต่อท่อต่อเมื่อคำสั่งก่อนหน้านี้สำเร็จ" ฉันคิดว่าคุณอาจมี|||ซึ่งจะหมายถึง "ดำเนินการต่อท่อหากคำสั่งก่อนหน้าล้มเหลว" (และอาจไพพ์ข้อความแสดงข้อผิดพลาดเช่น Bash 4's|&)