การจัดการข้อผิดพลาดใน Bash


240

อะไรคือวิธีที่คุณชื่นชอบในการจัดการข้อผิดพลาดใน Bash ตัวอย่างที่ดีที่สุดในการจัดการข้อผิดพลาดที่ฉันได้พบในเว็บเขียนโดยวิลเลียมช็อตจูเนียร์ที่http://www.linuxcommand.org

เขาแนะนำให้ใช้ฟังก์ชันต่อไปนี้สำหรับการจัดการข้อผิดพลาดใน Bash:

#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>

PROGNAME=$(basename $0)

function error_exit
{

#   ----------------------------------------------------------------
#   Function for exit due to fatal program error
#       Accepts 1 argument:
#           string containing descriptive error message
#   ---------------------------------------------------------------- 

    echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

คุณมีการจัดการข้อผิดพลาดที่ดีกว่าที่คุณใช้ในสคริปต์ Bash หรือไม่?


1
ดูรายละเอียดนี้คำตอบ: ข้อผิดพลาดในการยกสคริปต์ทุบตี
codeforester

1
ดูการใช้งานการบันทึกและการจัดการข้อผิดพลาดได้ที่นี่: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

คำตอบ:


154

ใช้กับดัก!

tempfiles=( )
cleanup() {
  rm -f "${tempfiles[@]}"
}
trap cleanup 0

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

... จากนั้นทุกครั้งที่คุณสร้างไฟล์ชั่วคราว:

temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )

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

คุณสามารถปล่อยให้กับดักเรียกerrorให้คุณ (ในกรณีนี้มันใช้รหัสออกเริ่มต้นของ 1 และไม่มีข้อความ) หรือเรียกมันว่าตัวเองและให้ค่าที่ชัดเจน; เช่น:

error ${LINENO} "the foobar failed" 2

จะออกจากสถานะ 2 และให้ข้อความชัดเจน


4
@draemon การใช้ตัวพิมพ์ใหญ่แบบแปรผันเป็นไปโดยเจตนา ตัวพิมพ์ใหญ่ทั้งหมดเป็นแบบธรรมดาสำหรับเชลล์บิวด์และตัวแปรสภาพแวดล้อม - การใช้ตัวพิมพ์เล็กสำหรับทุกอย่างอื่นป้องกันความขัดแย้งของเนมสเปซ ดูstackoverflow.com/questions/673055/…
Charles Duffy

1
ก่อนที่คุณจะทำลายมันอีกครั้งทดสอบการเปลี่ยนแปลงของคุณ การประชุมเป็นสิ่งที่ดี แต่มันก็เป็นสิ่งรองลงมาจากรหัสการทำงาน
Draemon

3
@ มังกรฉันไม่เห็นด้วยจริง เห็นได้ชัดว่ารหัสที่ใช้งานไม่ได้จะสังเกตและแก้ไขได้ การปฏิบัติที่ไม่ดี แต่รหัสส่วนใหญ่ทำงานอยู่ตลอดไป (และแพร่กระจาย)
Charles Duffy

1
แต่คุณไม่ได้สังเกต สังเกตเห็นรหัสที่ใช้งานไม่ได้เนื่องจากรหัสการทำงานเป็นปัญหาหลัก
Draemon

5
มันไม่ตรงไปตรงมา ( stackoverflow.com/a/10927223/26334 ) และถ้ารหัสนั้นเข้ากันไม่ได้กับ POSIX การลบคำหลักฟังก์ชั่นไม่ได้ทำให้มันทำงานได้ภายใต้ POSIX sh อีกต่อไป แต่ประเด็นหลักของฉันคือคุณ ' ve (IMO) ลดคำตอบโดยทำให้คำแนะนำการใช้ set -e อ่อนลง Stackoverflow ไม่ได้เกี่ยวกับรหัส "ของคุณ" แต่เกี่ยวกับการมีคำตอบที่ดีที่สุด
Draemon

123

นั่นเป็นทางออกที่ดี ฉันแค่อยากจะเพิ่ม

set -e

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


29
set -eไม่ได้โดยไม่มี gotchas: ดูmywiki.wooledge.org/BashFAQ/105สำหรับหลาย ๆ
Charles Duffy

3
@CharlesDuffy บางส่วนของ gotchas สามารถเอาชนะได้ด้วยset -o pipefail
hobs

7
@CharlesDuffy ขอบคุณที่ชี้ไปที่ gotchas; โดยรวมแม้ว่าฉันยังคิดว่าset -eมีอัตราส่วนต้นทุนผลประโยชน์สูง
Bruno De Fraine

3
@BrunoDeFraine ฉันใช้set -eตัวเอง แต่มีจำนวนประจำอื่น ๆ ใน irc.freenode.org # ทุบตีแนะนำ (ในแง่ที่แข็งแกร่งมาก) ต่อมัน อย่างน้อย gotchas ที่เป็นปัญหาควรมีความเข้าใจเป็นอย่างดี
Charles Duffy

3
ตั้ง -e -o pipefail -u # และรู้ว่าสิ่งที่คุณกำลังทำ
Sam Watkins

78

การอ่านคำตอบทั้งหมดในหน้านี้เป็นแรงบันดาลใจให้ฉันอย่างมาก

ดังนั้นนี่คือคำแนะนำของฉัน:

เนื้อหาไฟล์: lib.trap.sh

lib_name='trap'
lib_version=20121026

stderr_log="/dev/shm/stderr.log"

#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

if test "${g_libs[$lib_name]+_}"; then
    return 0
else
    if test ${#g_libs[@]} == 0; then
        declare -A g_libs
    fi
    g_libs[$lib_name]=$lib_version
fi


#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

exec 2>"$stderr_log"


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function exit_handler ()
{
    local error_code="$?"

    test $error_code == 0 && return;

    #
    # LOCAL VARIABLES:
    # ------------------------------------------------------------------
    #    
    local i=0
    local regex=''
    local mem=''

    local error_file=''
    local error_lineno=''
    local error_message='unknown'

    local lineno=''


    #
    # PRINT THE HEADER:
    # ------------------------------------------------------------------
    #
    # Color the output if it's an interactive terminal
    test -t 1 && tput bold; tput setf 4                                 ## red bold
    echo -e "\n(!) EXIT HANDLER:\n"


    #
    # GETTING LAST ERROR OCCURRED:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    #
    # Read last file from the error log
    # ------------------------------------------------------------------
    #
    if test -f "$stderr_log"
        then
            stderr=$( tail -n 1 "$stderr_log" )
            rm "$stderr_log"
    fi

    #
    # Managing the line to extract information:
    # ------------------------------------------------------------------
    #

    if test -n "$stderr"
        then        
            # Exploding stderr on :
            mem="$IFS"
            local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
            IFS=':'
            local stderr_parts=( $shrunk_stderr )
            IFS="$mem"

            # Storing information on the error
            error_file="${stderr_parts[0]}"
            error_lineno="${stderr_parts[1]}"
            error_message=""

            for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
                do
                    error_message="$error_message "${stderr_parts[$i-1]}": "
            done

            # Removing last ':' (colon character)
            error_message="${error_message%:*}"

            # Trim
            error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
    fi

    #
    # GETTING BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
    _backtrace=$( backtrace 2 )


    #
    # MANAGING THE OUTPUT:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    local lineno=""
    regex='^([a-z]{1,}) ([0-9]{1,})$'

    if [[ $error_lineno =~ $regex ]]

        # The error line was found on the log
        # (e.g. type 'ff' without quotes wherever)
        # --------------------------------------------------------------
        then
            local row="${BASH_REMATCH[1]}"
            lineno="${BASH_REMATCH[2]}"

            echo -e "FILE:\t\t${error_file}"
            echo -e "${row^^}:\t\t${lineno}\n"

            echo -e "ERROR CODE:\t${error_code}"             
            test -t 1 && tput setf 6                                    ## white yellow
            echo -e "ERROR MESSAGE:\n$error_message"


        else
            regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
            if [[ "$_backtrace" =~ $regex ]]

                # The file was found on the log but not the error line
                # (could not reproduce this case so far)
                # ------------------------------------------------------
                then
                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    echo -e "ERROR MESSAGE:\n${stderr}"

                # Neither the error line nor the error file was found on the log
                # (e.g. type 'cp ffd fdf' without quotes wherever)
                # ------------------------------------------------------
                else
                    #
                    # The error file is the first on backtrace list:

                    # Exploding backtrace on newlines
                    mem=$IFS
                    IFS='
                    '
                    #
                    # Substring: I keep only the carriage return
                    # (others needed only for tabbing purpose)
                    IFS=${IFS:0:1}
                    local lines=( $_backtrace )

                    IFS=$mem

                    error_file=""

                    if test -n "${lines[1]}"
                        then
                            array=( ${lines[1]} )

                            for (( i=2; i<${#array[@]}; i++ ))
                                do
                                    error_file="$error_file ${array[$i]}"
                            done

                            # Trim
                            error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
                    fi

                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    if test -n "${stderr}"
                        then
                            echo -e "ERROR MESSAGE:\n${stderr}"
                        else
                            echo -e "ERROR MESSAGE:\n${error_message}"
                    fi
            fi
    fi

    #
    # PRINTING THE BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 7                                            ## white bold
    echo -e "\n$_backtrace\n"

    #
    # EXITING:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 4                                            ## red bold
    echo "Exiting!"

    test -t 1 && tput sgr0 # Reset terminal

    exit "$error_code"
}
trap exit_handler EXIT                                                  # ! ! ! TRAP EXIT ! ! !
trap exit ERR                                                           # ! ! ! TRAP ERR ! ! !


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function backtrace
{
    local _start_from_=0

    local params=( "$@" )
    if (( "${#params[@]}" >= "1" ))
        then
            _start_from_="$1"
    fi

    local i=0
    local first=false
    while caller $i > /dev/null
    do
        if test -n "$_start_from_" && (( "$i" + 1   >= "$_start_from_" ))
            then
                if test "$first" == false
                    then
                        echo "BACKTRACE IS:"
                        first=true
                fi
                caller $i
        fi
        let "i=i+1"
    done
}

return 0



ตัวอย่างการใช้งาน:
เนื้อหาไฟล์: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

echo "doing something wrong now .."
echo "$foo"

exit 0


วิ่ง:

bash trap-test.sh

เอาท์พุท:

doing something wrong now ..

(!) EXIT HANDLER:

FILE:       trap-test.sh
LINE:       6

ERROR CODE: 1
ERROR MESSAGE:
foo:   unassigned variable

BACKTRACE IS:
1 main trap-test.sh

Exiting!


ดังที่คุณเห็นจากภาพหน้าจอด้านล่างผลลัพธ์เป็นสีและข้อความแสดงข้อผิดพลาดมาในภาษาที่ใช้

ป้อนคำอธิบายรูปภาพที่นี่


3
สิ่งนี้ยอดเยี่ยมมากคุณควรสร้างโครงการ GitHub เพื่อให้ผู้คนสามารถทำการปรับปรุงและช่วยเหลือได้อย่างง่ายดาย ฉันรวมมันกับ log4bash และร่วมกันสร้าง env ที่มีประสิทธิภาพสำหรับการสร้างสคริปต์ทุบตีที่ดี
Dominik Dorn

1
FYI - test ${#g_libs[@]} == 0ไม่สอดคล้องกับ POSIX (การทดสอบ POSIX รองรับ=การเปรียบเทียบสตริงหรือการเปรียบเทียบ-eqเป็นตัวเลข แต่ไม่==รวมถึงการขาดอาร์เรย์ใน POSIX) และหากคุณไม่ได้พยายามทำตาม POSIX ทำไมใน โลกนี้คุณใช้testอะไรมากกว่าบริบททางคณิตศาสตร์? (( ${#g_libs[@]} == 0 ))คือหลังจากทั้งหมดง่ายต่อการอ่าน
Charles Duffy

2
@Luca - นี่ยอดเยี่ยมจริงๆ! รูปภาพของคุณเป็นแรงบันดาลใจให้ฉันสร้างการใช้งานของตัวเองซึ่งจะนำไปสู่ขั้นตอนต่อไป ผมเคยโพสต์ไว้ในของฉันคำตอบด้านล่าง
niieani

3
Bravissimo !! นี่เป็นวิธีที่ดีในการดีบักสคริปต์ Grazie Milleสิ่งเดียวที่ฉันเพิ่มเป็นเช็คสำหรับ OS X เช่นนี้: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy

1
เสียบปลั๊กตัวเองที่ไร้ยางอาย แต่เราได้นำตัวอย่างนี้มาทำความสะอาดเพิ่มคุณสมบัติเพิ่มเติมปรับปรุงการจัดรูปแบบผลลัพธ์และทำให้รองรับ POSIX ได้มากขึ้น (ใช้ได้กับทั้ง Linux และ OSX) มันถูกเผยแพร่เป็นส่วนหนึ่งของ Privex ShellCore บน Github: github.com/Privex/shell-core
Someguy123

22

ทางเลือกที่เทียบเท่ากับ "set -e" คือ

set -o errexit

มันทำให้ความหมายของธงค่อนข้างชัดเจนกว่าเพียงแค่ "-e"

การเพิ่มแบบสุ่ม: หากต้องการปิดใช้งานการตั้งค่าสถานะชั่วคราวและกลับสู่ค่าเริ่มต้น (การดำเนินการต่อเนื่องโดยไม่คำนึงถึงรหัสออก) เพียงใช้

set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e

สิ่งนี้ขัดขวางการจัดการข้อผิดพลาดที่เหมาะสมที่กล่าวถึงในการตอบกลับอื่น ๆ แต่รวดเร็วและมีประสิทธิภาพ (เช่นทุบตี)


1
การใช้งาน$(foo)บนเส้นเปล่าแทนที่จะfooเป็นเพียงสิ่งผิดปกติ ทำไมต้องโปรโมตมันโดยให้มันเป็นตัวอย่าง?
Charles Duffy

20

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

เพียงแค่จัดหาไลบรารีคุณจะได้รับสิ่งต่อไปนี้ (เช่นจะหยุดการดำเนินการกับข้อผิดพลาดใด ๆ ราวกับว่าใช้set -eขอบคุณ a trapon ERRและbash-fuบางส่วน):

การจัดการข้อผิดพลาด bash-oo-framework

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

ข้อเสียคือ - ไม่ใช่แบบพกพา - รหัสใช้งานได้ใน bash ซึ่งอาจ> = 4 เท่านั้น (แต่ฉันคิดว่ามันอาจได้รับการรังเพลิงด้วยความพยายามในการทุบตี 3)

รหัสจะถูกแยกออกเป็นหลายไฟล์สำหรับการจัดการที่ดีขึ้น แต่ผมได้แรงบันดาลใจจากความคิดการติดตามย้อนหลังจากคำตอบข้างต้นโดย Luca Borrione

หากต้องการอ่านเพิ่มเติมหรือดูที่แหล่งข้อมูลให้ดู GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw


นี่คือภายในทุบตี Object Oriented กรอบโครงการ ... โชคดีที่มีเพียง 7.4k LOC (อ้างอิงจากGLOC ) OOP - ความเจ็บปวดเชิงวัตถุ
ingyhere

@ เนื่องจากมันเป็นแบบแยกส่วนสูง (และลบได้ง่าย) ดังนั้นคุณสามารถใช้ส่วนยกเว้นได้ถ้านั่นคือสิ่งที่คุณมา)
niieani

11

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

#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
    echo
    echo "$@"
    exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM

#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'

ฉันมักจะโทรไปที่ฟังก์ชั่นการล้างข้อมูลด้านข้างฟังก์ชั่น error_exit แต่สิ่งนี้แตกต่างกันไปในแต่ละสคริปต์ดังนั้นฉันจึงทิ้งมันไว้ กับดักจับสัญญาณที่ยุติทั่วไปและให้แน่ใจว่าทุกอย่างได้รับการทำความสะอาด นามแฝงคือสิ่งที่วิเศษจริง ฉันชอบที่จะตรวจสอบทุกอย่างสำหรับความล้มเหลว โดยทั่วไปแล้วฉันเรียกโปรแกรมใน "ถ้า!" พิมพ์คำสั่ง ด้วยการลบ 1 จากหมายเลขบรรทัดนามแฝงจะบอกฉันว่าเกิดความล้มเหลวที่ไหน นอกจากนี้ยังง่ายต่อการโทรตายและหลักฐานงี่เง่าสวยมาก ด้านล่างเป็นตัวอย่าง (เพียงแทนที่ / bin / false ด้วยสิ่งที่คุณกำลังจะเรียก)

#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
    die "Who knew false is false."
fi

2
คุณสามารถขยายคำสั่ง"เราต้องอนุญาตนามแฝงอย่างชัดเจน"หรือไม่? ฉันเป็นห่วงว่าพฤติกรรมที่ไม่คาดคิดบางอย่างอาจส่งผล มีวิธีในการบรรลุสิ่งเดียวกันโดยมีผลกระทบน้อยลงหรือไม่?
blong

$LINENO - 1ฉันไม่จำเป็นต้อง แสดงอย่างถูกต้องหากไม่มี
kyb

ตัวอย่างการใช้งานที่สั้นกว่าใน bash และ zshfalse || die "hello death"
kyb

6

สิ่งที่ต้องพิจารณาอีกประการคือรหัสทางออกที่จะส่งคืน เพียงแค่ " 1" นั้นเป็นมาตรฐานที่ค่อนข้างดีแม้ว่าจะมีรหัสทางออกที่สงวนไว้จำนวนหนึ่งซึ่งใช้ในการทุบตีและหน้าเดียวกันระบุว่ารหัสที่ผู้ใช้กำหนดควรอยู่ในช่วง 64-113 เพื่อให้สอดคล้องกับมาตรฐาน C / C ++

คุณอาจพิจารณาแนวทางบิตเวคเตอร์ที่mountใช้สำหรับรหัสออก:

 0  success
 1  incorrect invocation or permissions
 2  system error (out of memory, cannot fork, no more loop devices)
 4  internal mount bug or missing nfs support in mount
 8  user interrupt
16  problems writing or locking /etc/mtab
32  mount failure
64  some mount succeeded

ORการใช้รหัสร่วมกันทำให้สคริปต์ของคุณส่งสัญญาณข้อผิดพลาดหลาย ๆ


4

ฉันใช้รหัสกับดักต่อไปนี้มันยังอนุญาตให้มีการติดตามข้อผิดพลาดผ่านไพพ์และคำสั่ง 'เวลา'

#!/bin/bash
set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
function error() {
    JOB="$0"              # job name
    LASTLINE="$1"         # line of error occurrence
    LASTERR="$2"          # error code
    echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
    exit 1
}
trap 'error ${LINENO} ${?}' ERR

5
functionคำหลักคือจำเป็น POSIX-เข้ากันไม่ได้ พิจารณาการประกาศของคุณอย่างยุติธรรมerror() {โดยไม่functionมาก่อน
Charles Duffy

5
${$?}ควรเป็น$?หรือ${?}ถ้าคุณยืนยันที่จะใช้เครื่องมือจัดฟันที่ไม่จำเป็น ด้านใน$ผิด
Charles Duffy

3
@CharlesDuffy โดยตอนนี้ POSIX ไม่มีค่า GNU / Linux เข้ากันไม่ได้ (ยังไงก็ตามฉันขอให้คุณ)
Croad Langshan

3

ฉันเคยใช้

die() {
        echo $1
        kill $$
}

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


ส่งข้อความผิดพลาดไปที่ STDERR ได้ดีกว่าใช่ไหม
ankostis

3

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

# Custom errors
EX_UNKNOWN=1

warning()
{
    # Output warning messages
    # Color the output red if it's an interactive terminal
    # @param $1...: Messages

    test -t 1 && tput setf 4

    printf '%s\n' "$@" >&2

    test -t 1 && tput sgr0 # Reset terminal
    true
}

error()
{
    # Output error messages with optional exit code
    # @param $1...: Messages
    # @param $N: Exit code (optional)

    messages=( "$@" )

    # If the last parameter is a number, it's not part of the messages
    last_parameter="${messages[@]: -1}"
    if [[ "$last_parameter" =~ ^[0-9]*$ ]]
    then
        exit_code=$last_parameter
        unset messages[$((${#messages[@]} - 1))]
    fi

    warning "${messages[@]}"

    exit ${exit_code:-$EX_UNKNOWN}
}

3

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

#!/bin/bash

error_exit()
{
    if [ "$?" != "0" ]; then
        log.sh "$1"
        exit 1
    fi
}

ตอนนี้เพื่อเรียกมันภายในสคริปต์เดียวกัน (หรืออีกอันหนึ่งถ้าฉันใช้export -f error_exit) ฉันแค่เขียนชื่อของฟังก์ชั่นและส่งข้อความเป็นพารามิเตอร์เช่นนี้

#!/bin/bash

cd /home/myuser/afolder
error_exit "Unable to switch to folder"

rm *
error_exit "Unable to delete all files"

การใช้สิ่งนี้ฉันสามารถสร้างไฟล์ทุบตีที่แข็งแกร่งจริงๆสำหรับกระบวนการอัตโนมัติบางอย่างและมันจะหยุดในกรณีที่มีข้อผิดพลาดและแจ้งให้ฉันทราบ ( log.shจะทำ)


2
พิจารณาการใช้ไวยากรณ์ POSIX สำหรับการกำหนดฟังก์ชั่น - ไม่มีคำหลักเพียงfunction error_exit() {
Charles Duffy

2
มีเหตุผลทำไมคุณไม่ทำแค่cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares

@ Pierre-OlivierVares ไม่มีเหตุผลใดที่จะไม่ใช้ || นี่เป็นเพียงส่วนที่ตัดตอนมาของรหัสที่มีอยู่และฉันเพิ่งเพิ่มบรรทัด "การจัดการข้อผิดพลาด" หลังจากแต่ละบรรทัดที่เกี่ยวข้อง บางตัวมีความยาวมากและมันก็สะอาดกว่าที่จะเอามาแยกกัน (ทันที)
เนลสันโรดริเกซ

ดูเหมือนว่าวิธีการแก้ปัญหาที่สะอาด แต่การตรวจสอบเปลือกบ่น: github.com/koalaman/shellcheck/wiki/SC2181
mhulse

1

เคล็ดลับนี้มีประโยชน์สำหรับคำสั่งหรือฟังก์ชันที่หายไป ชื่อของฟังก์ชั่นที่หายไป (หรือปฏิบัติการ) จะถูกส่งผ่านใน $ _

function handle_error {
    status=$?
    last_call=$1

    # 127 is 'command not found'
    (( status != 127 )) && return

    echo "you tried to call $last_call"
    return
}

# Trap errors.
trap 'handle_error "$_"' ERR

จะไม่$_สามารถใช้ได้ในฟังก์ชั่นเหมือนกัน$?หรือไม่ ฉันไม่แน่ใจว่ามีเหตุผลใดที่จะใช้หนึ่งในฟังก์ชัน แต่ไม่ใช่เหตุผลอื่น
ingyhere

1

ฟังก์ชั่นนี้ให้บริการฉันค่อนข้างดีเมื่อเร็ว ๆ นี้:

action () {
    # Test if the first parameter is non-zero
    # and return straight away if so
    if test $1 -ne 0
    then
        return $1
    fi

    # Discard the control parameter
    # and execute the rest
    shift 1
    "$@"
    local status=$?

    # Test the exit status of the command run
    # and display an error message on failure
    if test ${status} -ne 0
    then
        echo Command \""$@"\" failed >&2
    fi

    return ${status}
}

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

command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...

กลายเป็นสิ่งนี้:

action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...

<<<Error-handling code here>>>

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


0

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

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

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

แต่||จำเป็นต้องใช้ตัวดำเนินการเพื่อป้องกันการส่งคืนจากฟังก์ชันภายนอกก่อนที่จะล้างข้อมูล เคล็ดลับคือการเรียกใช้คำสั่งด้านในในพื้นหลังแล้วรอทันที waitbuiltin จะกลับรหัสทางออกของคำสั่งภายในและตอนนี้คุณกำลังใช้||หลังจากที่waitไม่ได้ฟังก์ชั่นด้านในเพื่อให้set -eทำงานอย่างถูกต้องภายในหลัง:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

นี่คือฟังก์ชั่นทั่วไปที่สร้างขึ้นจากความคิดนี้ มันควรจะทำงานในเชลล์ที่เข้ากันได้กับ POSIX ทั้งหมดหากคุณลบlocalคำหลักออกนั่นคือแทนที่ทั้งหมดlocal x=yด้วยเพียงx=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

ตัวอย่างการใช้งาน:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

ใช้ตัวอย่าง:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

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

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