การจับรหัสข้อผิดพลาดในเชลล์ไปป์


98

ขณะนี้ฉันมีสคริปต์ที่ทำบางอย่างเช่น

./a | ./b | ./c

ฉันต้องการแก้ไขเพื่อที่ว่าถ้ามี a, b หรือ c ออกด้วยรหัสข้อผิดพลาดฉันพิมพ์ข้อความแสดงข้อผิดพลาดและหยุดแทนที่จะส่งผลลัพธ์ที่ไม่ดีไปข้างหน้า

อะไรคือวิธีที่ง่ายที่สุด / สะอาดที่สุดในการทำเช่นนั้น?


7
จำเป็นต้องมีบางอย่างเช่นนี้&&|ซึ่งหมายความว่า "ดำเนินการต่อท่อต่อเมื่อคำสั่งก่อนหน้านี้สำเร็จ" ฉันคิดว่าคุณอาจมี|||ซึ่งจะหมายถึง "ดำเนินการต่อท่อหากคำสั่งก่อนหน้าล้มเหลว" (และอาจไพพ์ข้อความแสดงข้อผิดพลาดเช่น Bash 4's |&)
หยุดชั่วคราวจนกว่าจะมีประกาศอีกครั้ง

6
@DennisWilliamson คุณไม่สามารถ "หยุดท่อ" เพราะa, b, cคำสั่งจะไม่ทำงานตามลำดับ แต่ในแบบคู่ขนาน ในคำอื่น ๆ กระแสข้อมูลตามลำดับจากaไปcแต่ที่เกิดขึ้นจริงa, bและcคำสั่งเริ่มต้น (ประมาณ) ในเวลาเดียวกัน
Giacomo

คำตอบ:


20

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

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' ไม่ได้ช่วยอะไร


1
ฉันอยากจะแนะนำให้ใช้mktempหรือtempfile.
หยุดชั่วคราวจนกว่าจะมีประกาศอีกครั้ง

@ เดนนิส: ใช่ฉันคิดว่าฉันควรจะคุ้นเคยกับคำสั่งเช่น mktemp หรือ tmpfile มันไม่ได้มีอยู่ในระดับเชลล์เมื่อฉันเรียนรู้มันโอ้หลายปีมาแล้ว มาตรวจสอบด่วน ฉันพบ mktemp บน MacOS X; ฉันมี mktemp บน Solaris แต่เพียงเพราะฉันติดตั้งเครื่องมือ GNU ดูเหมือนว่า mktemp มีอยู่ใน HP-UX โบราณ ฉันไม่แน่ใจว่ามีการเรียกใช้ mktemp ทั่วไปที่ทำงานข้ามแพลตฟอร์มหรือไม่ POSIX กำหนดมาตรฐานทั้ง mktemp หรือ tmpfile ฉันไม่พบ tmpfile บนแพลตฟอร์มที่ฉันสามารถเข้าถึงได้ ดังนั้นฉันจะใช้คำสั่งในเชลล์สคริปต์แบบพกพาไม่ได้
Jonathan Leffler

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

160

ในbashคุณสามารถใช้set -eและset -o pipefailที่จุดเริ่มต้นของไฟล์ของคุณ คำสั่งที่ตามมา./a | ./b | ./cจะล้มเหลวเมื่อสคริปต์ใด ๆ ในสามสคริปต์ล้มเหลว โค้ดส่งคืนจะเป็นโค้ดส่งคืนของสคริปต์แรกที่ล้มเหลว

โปรดทราบว่าpipefailไม่สามารถใช้ได้ในมาตรฐานการดวลจุดโทษ


ฉันไม่รู้เกี่ยวกับ pipefail มีประโยชน์จริงๆ
Phil Jackson

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

12
หมายเหตุ: สิ่งนี้จะยังคงดำเนินการทั้งสามสคริปต์และไม่หยุดไปป์เมื่อเกิดข้อผิดพลาดแรก
hyde

1
@ n2liquid-GuilhermeVieira Btw โดย "รูปแบบที่แตกต่างกัน" ฉันหมายถึงโดยเฉพาะให้ลบหนึ่งหรือทั้งสองอย่างออกset(รวม 4 เวอร์ชันที่แตกต่างกัน) และดูว่าสิ่งนั้นมีผลต่อผลลัพธ์ของเวอร์ชันล่าสุดechoอย่างไร
hyde

1
@josch ฉันพบหน้านี้เมื่อค้นหาวิธีทำใน bash จาก google และแม้ว่า "นี่คือสิ่งที่ฉันกำลังมองหา" ฉันสงสัยว่าหลายคนที่โหวตคำตอบต้องผ่านกระบวนการคิดที่คล้ายกันและไม่ได้ตรวจสอบแท็กและคำจำกัดความของแท็ก
Troy Daniels

45

คุณยังสามารถตรวจสอบ${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

จะช่วยให้คุณตรวจสอบว่าคำสั่งทั้งหมดในท่อสำเร็จ


11
นี่คือความเบื่อหน่าย - เป็นส่วนขยายของ bash และไม่ใช่ส่วนหนึ่งของมาตรฐาน Posix ดังนั้นกระสุนอื่น ๆ เช่นเส้นประและเถ้าจะไม่รองรับ ซึ่งหมายความว่าคุณอาจประสบปัญหาหากคุณพยายามใช้มันในสคริปต์ที่เริ่มต้น#!/bin/shเพราะถ้าshไม่ทุบตีมันจะไม่ทำงาน (แก้ไขได้ง่ายโดยจำใช้#!/bin/bashแทน)
David Given

3
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- ส่งคืน 0 หาก$PIPESTATUS[@]มีเพียง 0 และช่องว่าง (หากคำสั่งทั้งหมดในไปป์สำเร็จ)
MattBianco

@MattBianco นี่คือทางออกที่ดีที่สุดสำหรับฉัน นอกจากนี้ยังทำงานร่วมกับ && เช่นcommand1 && command2 | command3หากข้อใดล้มเหลวโซลูชันของคุณจะคืนค่าไม่เป็นศูนย์
DavidC

8

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