เพิ่มข้อผิดพลาดในสคริปต์ Bash


109

ฉันต้องการเพิ่มข้อผิดพลาดในสคริปต์ Bash พร้อมข้อความ "Test cases Failed !!!" วิธีการทำใน Bash?

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

if [ condition ]; then
    raise error "Test cases failed !!!"
fi

2
คุณต้องการให้เกิดอะไรขึ้นกับข้อผิดพลาดนี้ สคริปต์ของคุณเรียกว่าอย่างไร เป็นเพียงสคริปต์เดียวหรือหลายสคริปต์? การใช้สคริปต์ของคุณจะมีลักษณะอย่างไร
Etan Reisner

เพียงหนึ่งสคริปต์ ฉันเรียกมันโดยใช้เทอร์มินัล ubuntu เช่น. /script/test.sh
Naveen Kumar



ไม่มีความรักเพื่อecho you screwed up at ... | mail -s BUG $bugtrackeremailaddress?
infixed

คำตอบ:


126

ขึ้นอยู่กับตำแหน่งที่คุณต้องการจัดเก็บข้อความแสดงข้อผิดพลาด

คุณสามารถทำสิ่งต่อไปนี้:

echo "Error!" > logfile.log
exit 125

หรือดังต่อไปนี้:

echo "Error!" 1>&2
exit 64

เมื่อคุณเพิ่มข้อยกเว้นคุณจะหยุดการทำงานของโปรแกรม

คุณยังสามารถใช้บางอย่างเช่นรหัสข้อผิดพลาดที่คุณอาจต้องการกลับไปยังระบบปฏิบัติการexit xxxอยู่ที่ไหนxxx(จาก 0 ถึง 255) ที่นี่125และ64มีรหัสสุ่มเพียงคุณสามารถออกจากกับ เมื่อคุณจำเป็นต้องแสดงให้ระบบปฏิบัติการที่โปรแกรมหยุดผิดปกติ (เช่น. มีข้อผิดพลาดเกิดขึ้น) คุณจะต้องผ่านรหัสทางออกที่ไม่ใช่ศูนย์exitที่จะ

ในฐานะที่เป็น @chepner ชี้ให้เห็นคุณสามารถทำได้exit 1ซึ่งจะหมายถึงข้อผิดพลาดที่ไม่ได้ระบุ


12
หรือคุณสามารถส่งไปที่ stderr ซึ่งข้อผิดพลาดควรจะไป

จะส่งไปที่ stderr ได้อย่างไร?
Naveen Kumar

2
@ user3078630 ฉันเพิ่งแก้ไขคำตอบ 1>&2จะทำเคล็ดลับ
ForceBru

หากเป็นข้อผิดพลาดคุณควรออกด้วยสถานะการออกที่ไม่ใช่ศูนย์เช่นกัน exitโดยตัวมันเองใช้สถานะออกของคำสั่งที่เสร็จสมบูรณ์ล่าสุดซึ่งอาจเป็น 0
chepner

3
เว้นแต่คุณจะมีความหมายเฉพาะในใจคุณควรใช้exit 1ซึ่งตามแบบแผนหมายถึงข้อผิดพลาดที่ไม่ได้ระบุ
chepner

38

การจัดการข้อผิดพลาดพื้นฐาน

หากผู้ทดสอบกรณีทดสอบของคุณส่งคืนรหัสที่ไม่ใช่ศูนย์สำหรับการทดสอบที่ล้มเหลวคุณสามารถเขียน:

test_handler test_case_x; test_result=$?
if ((test_result != 0)); then
  printf '%s\n' "Test case x failed" >&2  # write error message to stderr
  exit 1                                  # or exit $test_result
fi

หรือสั้นกว่านั้น:

if ! test_handler test_case_x; then
  printf '%s\n' "Test case x failed" >&2
  exit 1
fi

หรือสั้นที่สุด:

test_handler test_case_x || { printf '%s\n' "Test case x failed" >&2; exit 1; }

ในการออกด้วยรหัสทางออกของ test_handler:

test_handler test_case_x || { ec=$?; printf '%s\n' "Test case x failed" >&2; exit $ec; }

การจัดการข้อผิดพลาดขั้นสูง

หากคุณต้องการใช้แนวทางที่ครอบคลุมมากขึ้นคุณสามารถมีตัวจัดการข้อผิดพลาด:

exit_if_error() {
  local exit_code=$1
  shift
  [[ $exit_code ]] &&               # do nothing if no error code passed
    ((exit_code != 0)) && {         # do nothing if error code is 0
      printf 'ERROR: %s\n' "$@" >&2 # we can use better logging here
      exit "$exit_code"             # we could also check to make sure
                                    # error code is numeric when passed
    }
}

จากนั้นเรียกใช้หลังจากเรียกใช้กรณีทดสอบของคุณ:

run_test_case test_case_x
exit_if_error $? "Test case x failed"

หรือ

run_test_case test_case_x || exit_if_error $? "Test case x failed"

ข้อดีของการมีตัวจัดการข้อผิดพลาดexit_if_errorคือ:

  • เราสามารถสร้างมาตรฐานตรรกะการจัดการข้อผิดพลาดทั้งหมดเช่นการบันทึกการพิมพ์การติดตามสแต็กการแจ้งเตือนการล้างข้อมูล ฯลฯ ในที่เดียว
  • โดยการทำให้ตัวจัดการข้อผิดพลาดรับรหัสข้อผิดพลาดเป็นอาร์กิวเมนต์เราสามารถสำรองผู้โทรจากความยุ่งเหยิงของifบล็อกที่ทดสอบรหัสทางออกเพื่อหาข้อผิดพลาด
  • หากเรามีตัวจัดการสัญญาณ (โดยใช้กับดัก ) เราสามารถเรียกใช้ตัวจัดการข้อผิดพลาดจากที่นั่น

เกิดข้อผิดพลาดในการจัดการและการบันทึกไลบรารี

นี่คือการใช้งานการจัดการและการบันทึกข้อผิดพลาดโดยสมบูรณ์:

https://github.com/codeforester/base/blob/master/lib/stdlib.sh


กระทู้ที่เกี่ยวข้อง


9

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

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

ดังนั้นสิ่งที่คุณต้องการจะทำขึ้นอยู่กับสองประเภทนี้

  • ออกจากข้อผิดพลาด
  • ออกและล้างข้อผิดพลาด

มีตัวเลือกเชลล์ให้ใช้ขึ้นอยู่กับว่าคุณต้องการทำอะไร สำหรับกรณีแรกที่เปลือกมีตัวเลือกที่มีset -eและเป็นครั้งที่สองที่คุณสามารถทำtrapบนEXIT

ฉันควรใช้exitในสคริปต์ / ฟังก์ชันของฉันหรือไม่?

การใช้exitโดยทั่วไปจะช่วยเพิ่มความสามารถในการอ่านในกิจวัตรบางอย่างเมื่อคุณทราบคำตอบแล้วคุณต้องการออกจากขั้นตอนการโทรทันที หากรูทีนถูกกำหนดในลักษณะที่ไม่จำเป็นต้องล้างข้อมูลเพิ่มเติมเมื่อตรวจพบข้อผิดพลาดการไม่ออกทันทีหมายความว่าคุณต้องเขียนโค้ดเพิ่มเติม

ดังนั้นในกรณีถ้าคุณจำเป็นต้องกระทำการทำความสะอาดขึ้นในสคริปต์เพื่อให้ยุติการทำความสะอาดสคริปต์ก็เป็นที่ต้องการไม่ได้exitกับการใช้งาน

ฉันควรใช้set -eสำหรับข้อผิดพลาดในการออกหรือไม่?

ไม่!

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

  • คำสั่งที่เป็นส่วนหนึ่งของการทดสอบ if มีภูมิคุ้มกัน ในตัวอย่างหากคุณคาดว่าจะทำลายการtestตรวจสอบในไดเร็กทอรีที่ไม่มีอยู่มันจะไม่ผ่านไปยังเงื่อนไขอื่น

    set -e
    f() { test -d nosuchdir && echo no dir; }
    f
    echo survived
    
  • คำสั่งในไปป์ไลน์นอกเหนือจากคำสั่งสุดท้ายมีภูมิคุ้มกัน ในตัวอย่างด้านล่างเนื่องจากโค้ดออกของคำสั่งที่เรียกใช้งานล่าสุด (ขวาสุด) ถือว่าเป็น ( cat) และทำได้สำเร็จ สิ่งนี้สามารถหลีกเลี่ยงได้โดยการตั้งค่าโดยset -o pipefailตัวเลือก แต่ยังคงเป็นข้อแม้

    set -e
    somecommand that fails | cat -
    echo survived 
    

แนะนำให้ใช้ - trapเมื่อออก

คำตัดสินคือถ้าคุณต้องการที่จะสามารถที่จะจัดการกับข้อผิดพลาดแทนที่จะสุ่มสี่สุ่มห้าออกแทนการใช้set -eใช้trapในERRสัญญาณหลอก

ERRดักไม่ได้ที่จะเรียกใช้รหัสเมื่อเปลือกของตัวเองออกมาด้วยไม่ใช่ศูนย์รหัสข้อผิดพลาด แต่เมื่อใดวิ่งคำสั่งจากเชลล์ที่ที่ไม่ได้เป็นส่วนหนึ่งของสภาพ (เช่นหากcmdหรือcmd ||) ออกกับสถานะทางออกที่ไม่ใช่ศูนย์ .

แนวปฏิบัติทั่วไปคือเรากำหนดตัวจัดการกับดักเพื่อให้ข้อมูลการดีบักเพิ่มเติมว่าบรรทัดใดและสาเหตุของการออก จำรหัสออกของคำสั่งสุดท้ายที่ทำให้เกิดERRสัญญาณจะยังคงมีอยู่ ณ จุดนี้

cleanup() {
    exitcode=$?
    printf 'error condition hit\n' 1>&2
    printf 'exit code returned: %s\n' "$exitcode"
    printf 'the command executing at the time of the error was: %s\n' "$BASH_COMMAND"
    printf 'command present on line: %d' "${BASH_LINENO[0]}"
    # Some more clean up code can be added here before exiting
    exit $exitcode
}

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

trap cleanup ERR

รวบรวมสิ่งนี้เข้าด้วยกันในสคริปต์ง่ายๆที่มีอยู่falseในบรรทัดที่ 15 ข้อมูลที่คุณจะได้รับ

error condition hit
exit code returned: 1
the command executing at the time of the error was: false
command present on line: 15

trapนอกจากนี้ยังมีตัวเลือกโดยไม่คำนึงถึงข้อผิดพลาดในการทำงานเพียงการล้างข้อมูลบนเปลือกเสร็จสิ้น (เช่นออกจากเชลล์สคริปต์ของคุณ) EXITกับสัญญาณ คุณยังสามารถดักจับสัญญาณหลายสัญญาณพร้อมกันได้ รายการสัญญาณที่รองรับเพื่อดักจับสามารถพบได้ในหน้าtrap.1p - คู่มือ Linux

สิ่งที่ควรสังเกตอีกประการหนึ่งคือต้องเข้าใจว่าวิธีการใดที่ให้มาไม่ได้ผลหากคุณจัดการกับเชลล์ย่อยที่เกี่ยวข้องในกรณีนี้คุณอาจต้องเพิ่มการจัดการข้อผิดพลาดของคุณเอง

  • บนเชลล์ย่อยที่ใช้set -eงานไม่ได้ falseถูก จำกัด ให้ย่อยเปลือกและไม่เคยได้รับการแพร่กระจายไปยังเปลือกแม่ หากต้องการจัดการข้อผิดพลาดที่นี่ให้เพิ่มตรรกะของคุณเองที่จะทำ(false) || false

    set -e
    (false)
    echo survived
    
  • เช่นเดียวกับที่เกิดขึ้นกับtrapยัง ตรรกะด้านล่างใช้ไม่ได้ด้วยเหตุผลที่กล่าวมาข้างต้น

    trap 'echo error' ERR
    (false)
    

5

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

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR

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

สำหรับฟังก์ชันการทำงานอย่างละเอียดให้ Trap เรียกใช้ฟังก์ชันการประมวลผล คุณสามารถใช้คำสั่ง case กับ arg ($ _) ของคุณได้ตลอดเวลาหากคุณต้องการล้างข้อมูลเพิ่มเติม ฯลฯ กำหนดให้ var สำหรับน้ำตาลที่เป็นประโยค -

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
throw=false
raise=false

while :
do x=$(( $RANDOM % 10 ))
   case "$x" in
   0) $throw "DIVISION BY ZERO" ;;
   3) $raise "MAGIC NUMBER"     ;;
   *) echo got $x               ;;
   esac
done

ตัวอย่างผลลัพธ์:

# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6

แน่นอนคุณทำได้

runTest1 "Test1 fails" # message not used if it succeeds

มีพื้นที่มากมายสำหรับการปรับปรุงการออกแบบ

ผลตอบแทนรวมถึงความจริงที่falseไม่สวย (เช่นน้ำตาล) และสิ่งอื่น ๆ ที่สะดุดกับดักอาจดูโง่เล็กน้อย ยังฉันชอบวิธีนี้


4

คุณมี 2 ตัวเลือก: เปลี่ยนเส้นทางเอาต์พุตของสคริปต์ไปยังไฟล์แนะนำไฟล์บันทึกในสคริปต์และ

  1. การเปลี่ยนทิศทางเอาต์พุตไปยังไฟล์ :

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

./runTests &> output.log

คำสั่งดังกล่าวเปลี่ยนเส้นทางทั้งเอาต์พุตมาตรฐานและเอาต์พุตข้อผิดพลาดไปยังไฟล์บันทึกของคุณ

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

  1. แนะนำไฟล์บันทึกไปยังสคริปต์ :

ในสคริปต์ของคุณให้เพิ่มไฟล์บันทึกโดยการเข้ารหัสอย่างหนัก:

logFile='./path/to/log/file.log'

หรือส่งผ่านพารามิเตอร์:

logFile="${1}"  # This assumes the first parameter to the script is the log file

เป็นความคิดที่ดีที่จะเพิ่มการประทับเวลาในขณะดำเนินการลงในไฟล์บันทึกที่ด้านบนของสคริปต์:

date '+%Y%-m%d-%H%M%S' >> "${logFile}"

จากนั้นคุณสามารถเปลี่ยนเส้นทางข้อความแสดงข้อผิดพลาดของคุณไปยังไฟล์บันทึก

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
fi

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

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
    # Clean up if needed
    exit 1;
fi

โปรดสังเกตว่าexit 1โปรแกรมหยุดดำเนินการเนื่องจากข้อผิดพลาดที่ไม่ได้ระบุ คุณสามารถปรับแต่งสิ่งนี้ได้หากต้องการ

เมื่อใช้วิธีนี้คุณสามารถปรับแต่งบันทึกของคุณและมีไฟล์บันทึกที่แตกต่างกันสำหรับแต่ละองค์ประกอบของสคริปต์ของคุณ


หากคุณมีสคริปต์ที่ค่อนข้างเล็กหรือต้องการเรียกใช้สคริปต์ของผู้อื่นโดยไม่แก้ไขให้เป็นแนวทางแรกจะเหมาะสมกว่า

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


3

ฉันมักพบว่ามีประโยชน์ในการเขียนฟังก์ชันเพื่อจัดการข้อความแสดงข้อผิดพลาดเพื่อให้โค้ดนั้นสะอาดขึ้นโดยรวม

# Usage: die [exit_code] [error message]
die() {
  local code=$? now=$(date +%T.%N)
  if [ "$1" -ge 0 ] 2>/dev/null; then  # assume $1 is an error code if numeric
    code="$1"
    shift
  fi
  echo "$0: ERROR at ${now%???}${1:+: $*}" >&2
  exit $code
}

สิ่งนี้ใช้รหัสข้อผิดพลาดจากคำสั่งก่อนหน้าและใช้เป็นรหัสข้อผิดพลาดเริ่มต้นเมื่อออกจากสคริปต์ทั้งหมด นอกจากนี้ยังบันทึกเวลาด้วยไมโครวินาทีที่รองรับ (วันที่ของ GNU %Nคือนาโนวินาทีซึ่งเราจะตัดทอนเป็นไมโครวินาทีในภายหลัง)

หากตัวเลือกแรกเป็นศูนย์หรือจำนวนเต็มบวกจะกลายเป็นรหัสออกและเราจะลบออกจากรายการตัวเลือก จากนั้นเราจะรายงานข้อความเป็นข้อผิดพลาดมาตรฐานโดยใช้ชื่อของสคริปต์คำว่า "ERROR" และเวลา (เราใช้การขยายพารามิเตอร์เพื่อตัดทอนนาโนวินาทีเป็นไมโครวินาทีหรือสำหรับเวลาที่ไม่ใช่ GNU เพื่อตัดทอนเช่น12:34:56.%Nถึง12:34:56) เครื่องหมายจุดคู่และช่องว่างจะถูกเพิ่มหลังคำว่า ERROR แต่เมื่อมีข้อความแสดงข้อผิดพลาดที่ให้มาเท่านั้น สุดท้ายเราออกจากสคริปต์โดยใช้รหัสทางออกที่กำหนดไว้ก่อนหน้านี้ทำให้เกิดกับดักตามปกติ

ตัวอย่างบางส่วน (สมมติว่ารหัสอยู่ในscript.sh):

if [ condition ]; then die 123 "condition not met"; fi
# exit code 123, message "script.sh: ERROR at 14:58:01.234564: condition not met"

$command |grep -q condition || die 1 "'$command' lacked 'condition'"
# exit code 1, "script.sh: ERROR at 14:58:55.825626: 'foo' lacked 'condition'"

$command || die
# exit code comes from command's, message "script.sh: ERROR at 14:59:15.575089"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.