Xử lý lỗi trong Bash


239

Phương pháp yêu thích của bạn để xử lý lỗi trong Bash là gì? Ví dụ tốt nhất về xử lý lỗi tôi đã tìm thấy trên web được viết bởi William Shotts, Jr tại http://www.linuxcommand.org .

Ông đề nghị sử dụng hàm sau để xử lý lỗi trong 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."

Bạn có thói quen xử lý lỗi tốt hơn mà bạn sử dụng trong các tập lệnh Bash không?


1
Xem câu trả lời chi tiết này: Tăng lỗi trong tập lệnh Bash .
codeforester

1
Xem việc thực hiện ghi nhật ký và xử lý lỗi tại đây: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Câu trả lời:


154

Sử dụng một cái bẫy!

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

... Sau đó, bất cứ khi nào bạn tạo một tệp tạm thời:

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

$temp_foosẽ bị xóa khi thoát và số dòng hiện tại sẽ được in. ( set -etương tự sẽ cung cấp cho bạn hành vi thoát lỗi, mặc dù nó đi kèm với cảnh báo nghiêm trọng và làm suy yếu khả năng dự đoán và tính di động của mã).

Bạn có thể để bẫy gọi errorcho bạn (trong trường hợp đó, nó sử dụng mã thoát mặc định là 1 và không có tin nhắn) hoặc tự gọi nó và cung cấp các giá trị rõ ràng; ví dụ:

error ${LINENO} "the foobar failed" 2

sẽ thoát với trạng thái 2 và đưa ra một thông báo rõ ràng.


4
@draemon viết hoa biến là cố ý. All-caps chỉ thông thường cho các nội dung shell và biến môi trường - sử dụng chữ thường cho mọi thứ khác ngăn ngừa xung đột không gian tên. Xem thêm stackoverflow.com/questions/673055/ Mạnh
Charles Duffy

1
trước khi bạn phá vỡ nó một lần nữa, kiểm tra sự thay đổi của bạn. Các quy ước là một điều tốt, nhưng chúng là thứ yếu đối với mã chức năng.
Draemon

3
@Draemon, tôi thực sự không đồng ý. Rõ ràng mã bị hỏng được chú ý và sửa chữa. Mã thực hành xấu nhưng chủ yếu là mã làm việc tồn tại mãi mãi (và được truyền bá).
Charles Duffy

1
nhưng bạn đã không thông báo. Mã bị hỏng được chú ý mã chức năng là mối quan tâm chính.
Draemon

5
nó không chính xác cho không ( stackoverflow.com/a/10927223/26334 ) và nếu mã đã không tương thích với POSIX loại bỏ các từ khóa chức năng không làm cho nó bất kỳ khả năng hơn để chạy theo POSIX sh, nhưng điểm chính của tôi là bạn' ve (IMO) làm giảm giá trị câu trả lời bằng cách làm suy yếu khuyến nghị sử dụng set -e. Stackoverflow không phải là về mã "của bạn", đó là về việc có câu trả lời tốt nhất.
Draemon

123

Đó là một giải pháp tốt. Tôi chỉ muốn thêm

set -e

như một cơ chế lỗi thô sơ. Nó sẽ ngay lập tức dừng tập lệnh của bạn nếu một lệnh đơn giản thất bại. Tôi nghĩ rằng đây đáng lẽ phải là hành vi mặc định: vì những lỗi như vậy hầu như luôn biểu thị điều gì đó bất ngờ, nên việc thực hiện các lệnh sau đây không thực sự 'lành mạnh'.


29
set -ekhông phải là không có gotchas: Xem mywiki.wooledge.org/BashFAQ/105 để biết thêm.
Charles Duffy

3
@CharlesDuffy, một số vấn đề có thể khắc phục bằngset -o pipefail
hobs

7
@CharlesDuffy Cảm ơn bạn đã chỉ vào gotchas; Nhìn chung, tôi vẫn nghĩ rằng set -ecó tỷ lệ chi phí lợi ích cao.
Bruno De Fraine

3
@BrunoDeFraine Tôi sử dụng set -ebản thân mình, nhưng một số quy định khác trong irc.freenode.org # bash khuyên (trong điều khoản khá mạnh) chống lại nó. Ở mức tối thiểu, các vấn đề về vấn đề nên được hiểu rõ.
Charles Duffy

3
đặt -e -o pipefail -u # và biết những gì bạn đang làm
Sam Watkins

78

Đọc tất cả các câu trả lời trên trang này đã truyền cảm hứng cho tôi rất nhiều.

Vì vậy, đây là gợi ý của tôi:

nội dung tệp: 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



Ví dụ về cách sử dụng:
nội dung tệp: bẫy-test.sh

#!/bin/bash

source 'lib.trap.sh'

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

exit 0


Đang chạy:

bash trap-test.sh

Đầu ra:

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!


Như bạn có thể thấy từ ảnh chụp màn hình bên dưới, đầu ra được tô màu và thông báo lỗi xuất hiện trong ngôn ngữ được sử dụng.

nhập mô tả hình ảnh ở đây


3
điều này thật tuyệt vời .. bạn nên tạo một dự án github cho nó, để mọi người có thể dễ dàng cải thiện và đóng góp lại. Tôi đã kết hợp nó với log4bash và cùng nhau nó tạo ra một env mạnh mẽ để tạo ra các tập lệnh bash tốt.
Dominik Dorn

1
FYI - test ${#g_libs[@]} == 0không tuân thủ POSIX (kiểm tra POSIX hỗ trợ =so sánh chuỗi hoặc so sánh -eqsố, nhưng không ==đề cập đến việc thiếu mảng trong POSIX) và nếu bạn không cố gắng tuân thủ POSIX, tại sao trong bạn đang sử dụng thế giới testchứ không phải là một bối cảnh toán học? (( ${#g_libs[@]} == 0 ))Rốt cuộc, dễ đọc hơn.
Charles Duffy

2
@Luca - điều này thực sự tuyệt vời! Hình ảnh của bạn đã truyền cảm hứng cho tôi để tạo ra việc thực hiện điều này của riêng tôi, điều này thậm chí còn tiến thêm một vài bước nữa. Tôi đã đăng nó trong câu trả lời của tôi dưới đây .
niieani

3
Bravissimo !! Đây là một cách tuyệt vời để gỡ lỗi một kịch bản. Grazie mille Điều duy nhất tôi thêm vào là kiểm tra OS X như thế này: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy 27/8/2016

1
Một chút tự cắm không biết xấu hổ, nhưng chúng tôi đã lấy đoạn mã này, dọn sạch nó, thêm nhiều tính năng hơn, cải thiện định dạng đầu ra và làm cho nó tương thích POSIX hơn (hoạt động trên cả Linux và OSX). Nó được xuất bản như một phần của Privex ShellCore trên Github: github.com/Privex/shell-core
someguy123

22

Một thay thế tương đương với "set -e" là

set -o errexit

Nó làm cho ý nghĩa của cờ có phần rõ ràng hơn là chỉ "-e".

Bổ sung ngẫu nhiên: để tạm thời vô hiệu hóa cờ và trở về mặc định (thực hiện tiếp tục bất kể mã thoát), chỉ cần sử dụng

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

Điều này ngăn chặn xử lý lỗi thích hợp được đề cập trong các phản hồi khác, nhưng nhanh chóng và hiệu quả (giống như bash).


1
sử dụng $(foo)trên một dòng trần thay vì chỉ foothường là Điều sai. Tại sao thúc đẩy nó bằng cách đưa ra nó như một ví dụ?
Charles Duffy

20

Lấy cảm hứng từ các ý tưởng được trình bày ở đây, tôi đã phát triển một cách dễ đọc và thuận tiện để xử lý các lỗi trong các tập lệnh bash trong dự án soạn sẵn bash của tôi .

Bằng cách đơn giản là tìm nguồn cung cấp thư viện, bạn sẽ có được những thứ sau đây (nghĩa là nó sẽ tạm dừng thực thi bất kỳ lỗi nào, như thể sử dụng set -enhờ vào một trapon ERRvà một số bash-fu ):

xử lý lỗi bash-oo-framework

Có một số tính năng bổ sung giúp xử lý lỗi, chẳng hạn như thử và bắt hoặc từ khóa throw , cho phép bạn ngắt thực thi tại một điểm để xem backtrace. Thêm vào đó, nếu thiết bị đầu cuối hỗ trợ nó, nó sẽ tạo ra các biểu tượng cảm xúc powerline, tô màu các phần của đầu ra để dễ đọc và gạch chân phương thức gây ra ngoại lệ trong ngữ cảnh của dòng mã.

Nhược điểm là - nó không khả dụng - mã chỉ hoạt động trong bash, có thể chỉ> = 4 (nhưng tôi tưởng tượng nó có thể được chuyển với một số nỗ lực để bash 3).

Mã được tách thành nhiều tệp để xử lý tốt hơn, nhưng tôi đã được truyền cảm hứng từ ý tưởng backtrace từ câu trả lời ở trên của Luca Borrione .

Để đọc thêm hoặc xem nguồn, xem GitHub:

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


Đây là bên trong dự án Khung hướng đối tượng Bash . ... May mắn thay, nó chỉ có 7.4k LỘC (theo GLOC ). OOP - Đau theo hướng đối tượng?
ingyhere

@ingyhere nó có tính mô-đun cao (và thân thiện với việc xóa), vì vậy bạn chỉ có thể sử dụng phần ngoại lệ nếu đó là những gì bạn đã làm;)
niieani

11

Tôi thích một cái gì đó thực sự dễ dàng để gọi. Vì vậy, tôi sử dụng một cái gì đó trông hơi phức tạp, nhưng dễ sử dụng. Tôi thường chỉ sao chép và dán mã dưới đây vào tập lệnh của mình. Một lời giải thích theo mã.

#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 ))`):"'

Tôi thường đặt một cuộc gọi đến chức năng dọn dẹp bên cạnh hàm error_exit, nhưng điều này thay đổi từ tập lệnh sang tập lệnh vì vậy tôi đã bỏ nó. Các bẫy bắt các tín hiệu kết thúc phổ biến và đảm bảo mọi thứ sẽ được dọn sạch. Bí danh là những gì làm phép thuật thực sự. Tôi thích kiểm tra mọi thứ cho sự thất bại. Vì vậy, nói chung tôi gọi các chương trình trong một "nếu!" loại tuyên bố. Bằng cách trừ 1 từ số dòng, bí danh sẽ cho tôi biết lỗi xảy ra ở đâu. Nó cũng đơn giản để gọi, và khá nhiều bằng chứng ngu ngốc. Dưới đây là một ví dụ (chỉ cần thay thế / bin / false bằng bất cứ điều gì bạn sẽ gọi).

#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
Bạn có thể mở rộng về tuyên bố "Chúng tôi phải cho phép rõ ràng bí danh" không? Tôi lo lắng rằng một số hành vi bất ngờ có thể dẫn đến. Có cách nào để đạt được điều tương tự với tác động nhỏ hơn không?
blong 29/07/2015

Tôi không cần $LINENO - 1. Hiển thị chính xác mà không có nó.
kyb

Ví dụ sử dụng ngắn hơn trong bash và zshfalse || die "hello death"
kyb

6

Một xem xét khác là mã thoát để trả lại. Chỉ " 1" là khá chuẩn, mặc dù có một số mã thoát được bảo lưu mà chính bash sử dụng và cùng một trang lập luận rằng các mã do người dùng xác định phải nằm trong phạm vi 64-113 để tuân thủ các tiêu chuẩn C / C ++.

Bạn cũng có thể xem xét cách tiếp cận vectơ bit mountsử dụng cho mã thoát của nó:

 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-ing các mã với nhau cho phép tập lệnh của bạn báo hiệu nhiều lỗi đồng thời.


4

Tôi sử dụng mã bẫy sau đây, nó cũng cho phép các lỗi được truy tìm thông qua các đường ống và lệnh 'thời gian'

#!/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
Các functiontừ khóa là cách nhưng không POSIX-không tương thích. Xem xét việc khai báo của bạn chỉ error() {, không có functiontrước nó.
Charles Duffy

5
${$?}chỉ nên $?, hoặc ${?}nếu bạn khăng khăng sử dụng niềng răng không cần thiết; bên trong $là sai.
Charles Duffy

3
@CharlesDuffy bây giờ, POSIX không tương thích với GNU / Linux (vẫn vậy, tôi nêu quan điểm của bạn)
Croad Langshan

3

Tôi đã sử dụng

die() {
        echo $1
        kill $$
}

trước; Tôi nghĩ bởi vì "lối ra" đã thất bại đối với tôi vì một số lý do. Mặc định ở trên có vẻ như là một ý tưởng tốt, mặc dù.


Tốt hơn nên gửi tin nhắn err tới STDERR, không?
ankostis

3

Điều này đã phục vụ tôi tốt trong một thời gian bây giờ. Nó in thông báo lỗi hoặc cảnh báo bằng màu đỏ, một dòng trên mỗi tham số và cho phép mã thoát tùy chọn.

# 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

Không chắc điều này có hữu ích với bạn không, nhưng tôi đã sửa đổi một số chức năng được đề xuất ở đây để bao gồm kiểm tra lỗi (mã thoát khỏi lệnh trước) trong đó. Trên mỗi "kiểm tra", tôi cũng chuyển như một tham số "thông báo" về lỗi là gì cho mục đích ghi nhật ký.

#!/bin/bash

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

Bây giờ để gọi nó trong cùng một tập lệnh (hoặc trong một tập lệnh khác nếu tôi sử dụng export -f error_exit) Tôi chỉ cần viết tên của hàm và truyền một thông điệp dưới dạng tham số, như thế này:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

Sử dụng điều này tôi đã có thể tạo một tệp bash thực sự mạnh mẽ cho một số quy trình tự động và nó sẽ dừng trong trường hợp có lỗi và thông báo cho tôi ( log.shsẽ làm điều đó)


2
Cân nhắc sử dụng cú pháp POSIX để xác định hàm - không có functiontừ khóa error_exit() {.
Charles Duffy

2
Có một lý do tại sao bạn không chỉ làm cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares

@ Pierre-OlivierVares Không có lý do cụ thể nào về việc không sử dụng ||. Đây chỉ là một đoạn trích của một mã hiện có và tôi chỉ thêm các dòng "xử lý lỗi" sau mỗi dòng liên quan. Một số rất dài và nó chỉ sạch hơn khi có nó trên một dòng riêng (ngay lập tức)
Nelson Rodriguez

Tuy nhiên, có vẻ như một giải pháp sạch sẽ, kiểm tra vỏ phàn nàn: github.com/koalaman/shellcheck/wiki/SC2181
mhulse

1

Thủ thuật này rất hữu ích cho các lệnh hoặc chức năng bị thiếu. Tên của hàm bị thiếu (hoặc có thể thực thi) sẽ được chuyển qua $ _

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

Sẽ không $_có sẵn trong chức năng giống như $?? Tôi không chắc chắn có bất kỳ lý do để sử dụng một trong các chức năng nhưng không phải là lý do khác.
ingyhere

1

Chức năng này đã phục vụ tôi khá tốt gần đây:

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}
}

Bạn gọi nó bằng cách nối thêm 0 hoặc giá trị trả về cuối cùng vào tên của lệnh sẽ chạy, do đó bạn có thể xâu chuỗi các lệnh mà không phải kiểm tra các giá trị lỗi. Với điều này, khối tuyên bố này:

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

Trở thành này:

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>>>

Nếu bất kỳ lệnh nào thất bại, mã lỗi chỉ đơn giản được chuyển đến cuối khối. Tôi thấy nó hữu ích khi bạn không muốn các lệnh tiếp theo thực thi nếu một lệnh trước đó thất bại, nhưng bạn cũng không muốn tập lệnh thoát ngay lập tức (ví dụ, bên trong một vòng lặp).


0

Sử dụng bẫy không phải lúc nào cũng là một lựa chọn. Ví dụ: nếu bạn đang viết một loại chức năng có thể sử dụng lại cần xử lý lỗi và có thể được gọi từ bất kỳ tập lệnh nào (sau khi tìm nguồn tệp với các hàm trợ giúp), chức năng đó không thể thừa nhận bất cứ điều gì về thời gian thoát của tập lệnh bên ngoài, Điều này làm cho việc sử dụng bẫy rất khó khăn. Một nhược điểm khác của việc sử dụng bẫy là khả năng kết hợp kém, vì bạn có nguy cơ ghi đè lên bẫy trước đó có thể được thiết lập trước đó trong chuỗi người gọi.

Có một mẹo nhỏ có thể được sử dụng để xử lý lỗi thích hợp mà không cần bẫy. Như bạn có thể đã biết từ các câu trả lời khác, set -ekhông hoạt động bên trong các lệnh nếu bạn sử dụng ||toán tử sau chúng, ngay cả khi bạn chạy chúng trong một mạng con; ví dụ, điều này sẽ không hoạt động:

#!/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

Nhưng người ||vận hành là cần thiết để ngăn trở về từ chức năng bên ngoài trước khi dọn dẹp. Bí quyết là chạy lệnh bên trong trong nền, và sau đó đợi nó ngay lập tức. Nội dung waitsẽ trả về mã thoát của lệnh bên trong và bây giờ bạn đang sử dụng hàm ||sau wait, không phải hàm bên trong, do đó set -ehoạt động chính xác bên trong lệnh sau:

#!/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

Đây là chức năng chung dựa trên ý tưởng này. Nó sẽ hoạt động trong tất cả các shell tương thích POSIX nếu bạn xóa localtừ khóa, tức là thay thế tất cả local x=ychỉ bằng 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
}

Ví dụ về cách sử dụng:

#!/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 "$@"

Chạy ví dụ:

$ ./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

Điều duy nhất mà bạn cần lưu ý khi sử dụng phương thức này là tất cả các sửa đổi của các biến Shell được thực hiện từ lệnh bạn truyền đến runsẽ không truyền đến hàm gọi, bởi vì lệnh chạy trong một lớp con.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.