it-swarm-vi.tech

Xử lý lỗi trong Bash

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?

211
Noob

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_foo sẽ bị xóa khi thoát và số dòng hiện tại sẽ được in. (set -e cũng 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 error cho 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.

145
Charles Duffy

Đó 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'.

112
Bruno De Fraine

Đọ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=( "[email protected]" )
    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ó màu và thông báo lỗi xuất hiện trong ngôn ngữ được sử dụng.

enter image description here

71
Luca Borrione

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

22
Ben Scholbrock

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 bash cái lò nướng .

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ẽ dừng thực thi bất kỳ lỗi nào, như thể sử dụng set -e nhờ vào một trap trên ERR và một số bash-fu )

bash-oo-framework error handling

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 di động - mã chỉ hoạt động trong bash, có lẽ 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 đã lấy 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

19
niieani

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 "[email protected]"
    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 là đơn giản để gọi, và bằng chứng khá nhiều 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
11
Michael Nooner

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 dành riêng mà bash tự 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ủ Tiêu chuẩn C/C++.

Bạn cũng có thể xem xét cách tiếp cận vectơ bit mà mount sử 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ã cùng nhau cho phép tập lệnh của bạn báo hiệu nhiều lỗi đồng thời.

6
yukondude

Tôi sử dụng mã bẫy sau, nó cũng cho phép 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
4
Olivier Delrieu

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

3
pjz

Cái này đã phục vụ tôi rất lâu rồi. 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' "[email protected]" >&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=( "[email protected]" )

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

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ư sau:

#!/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.sh sẽ làm điều đó)

2
Nelson Rodriguez

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
    "[email protected]"
    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 \""[email protected]"\" 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).

1
xarxziux

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

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 -e khô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 toán tử || 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 đó ngay lập tức chờ đợi nó. Nội dung wait sẽ trả về mã thoát của lệnh bên trong và bây giờ bạn đang sử dụng || sau wait, không phải hàm bên trong, vì vậy set -e hoạt động chính xác bên trong 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 hệ vỏ tương thích POSIX nếu bạn xóa local từ khóa, tức là thay thế tất cả local x=y chỉ 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" "[email protected]" &

  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" "[email protected]"
    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: [email protected]"
  CLEANUP=cleanup run inner "[email protected]"
  echo "<-- main"
}


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


cleanup() {
  echo "--> cleanup: [email protected]"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "[email protected]"

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 tới run sẽ không truyền đến hàm gọi, bởi vì lệnh chạy trong một lớp con.

0
skozin