Bash 中的错误处理

Posted

技术标签:

【中文标题】Bash 中的错误处理【英文标题】:Error handling in Bash [closed] 【发布时间】:2010-09-09 01:13:36 【问题描述】:

您最喜欢在 Bash 中处理错误的方法是什么? 我在网上找到的处理错误的最佳示例是由 William Shotts, Jr 在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 脚本中使用了更好的错误处理例程吗?

【问题讨论】:

查看详细答案:Raise error in a Bash script。 在此处查看日志记录和错误处理实现:github.com/codeforester/base/blob/master/lib/stdlib.sh 【参考方案1】:

使用陷阱!

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 也会给你提供错误退出行为,though it comes with serious caveats 并削弱代码的可预测性和可移植性)。

您可以让陷阱为您调用error(在这种情况下,它使用默认退出代码 1 并且没有消息)或自己调用它并提供显式值;例如:

error $LINENO "the foobar failed" 2

将以状态 2 退出,并给出明确的消息。

【讨论】:

@draemon 变量大写是有意的。全大写仅适用于 shell 内置函数和环境变量——对其他所有内容使用小写字母可防止命名空间冲突。另请参阅***.com/questions/673055/… 在你再次破坏它之前,测试你的改变。约定是一件好事,但它们对功能代码来说是次要的。 @Draemon,我实际上不同意。明显损坏的代码会被注意到并修复。糟糕的做法,但大部分工作的代码永远存在(并得到传播)。 这不是完全免费的 (***.com/a/10927223/26334),如果代码已经与 POSIX 不兼容,删除 function 关键字不会使其更能够在 POSIX sh 下运行,但我的主要观点是您(IMO)通过削弱使用 set -e 的建议来贬低答案。 *** 不是关于“你的”代码,而是关于获得最佳答案。 对于那些今天阅读这篇文章的人:一些最新版本的 bash 在陷阱处理程序中存在影响 LINENO 准确性的错误。因此,在某些情况下,在过去它曾经可以正常工作的情况下今天将无法正常工作。【参考方案2】:

这是一个很好的解决方案。我只是想补充

set -e

作为一种基本的错误机制。如果一个简单的命令失败,它将立即停止您的脚本。我认为这应该是默认行为:由于此类错误几乎总是意味着意外情况,因此继续执行以下命令并不真正“理智”。

【讨论】:

set -e 并非没有陷阱:请参阅mywiki.wooledge.org/BashFAQ/105 了解几个。 @CharlesDuffy,有些问题可以通过set -o pipefail来克服 @CharlesDuffy 感谢您指出问题;不过总的来说,我仍然认为set -e 的收益成本比很高。 @BrunoDeFraine 我自己使用set -e,但 irc.freenode.org#bash 中的一些其他常客建议(以非常强烈的措辞)反对它。至少,所讨论的问题应该很好理解。 set -e -o pipefail -u # 知道你在做什么【参考方案3】:

阅读此页面上的所有答案给了我很多启发。 所以,这是我的提示:文件内容: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!

正如您从下面的屏幕截图中看到的那样,输出是彩色的,并且错误消息以使用的语言显示。

【讨论】:

这东西太棒了.. 你应该为它创建一个 github 项目,这样人们就可以轻松地进行改进和回馈。我将它与 log4bash 结合在一起,它为创建良好的 bash 脚本创建了一个强大的环境。 仅供参考 -- test $#g_libs[@] == 0 不符合 POSIX(POSIX 测试支持 = 进行字符串比较或 -eq 进行数字比较,但不支持 ==,更不用说缺少POSIX 中的数组),如果您试图符合 POSIX,那么您到底为什么要使用 test 而不是数学上下文?毕竟(( $#g_libs[@] == 0 )) 更容易阅读。 @Luca - 这真是太棒了!你的照片启发了我创建自己的实现,这甚至更进一步。我已将其发布在我的answer below。 布拉维西莫!!这是调试脚本的绝佳方式。 Grazie mille 我唯一添加的是对 OS X 的检查,如下所示:case "$(uname)" in Darwin ) stderr_log="$TMPDIRstderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac 有点无耻的自插件,但我们已经采用了这个 sn-p,对其进行了清理,添加了更多功能,改进了输出格式,并使其更兼容 POSIX(适用于两者Linux 和 OSX)。它作为 Privex ShellCore 的一部分在 Github 上发布:github.com/Privex/shell-core【参考方案4】:

“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

这排除了其他响应中提到的正确错误处理,但快速有效(就像 bash)。

【讨论】:

在裸行上使用$(foo) 而不仅仅是foo 通常是错误的。为什么要举个例子来宣传?【参考方案5】:

受这里提出的想法的启发,我在bash boilerplate project 中开发了一种可读且方便的方法来处理 bash 脚本中的错误。

通过简单地采购库,您可以立即获得以下内容(即,它会在任何错误时停止执行,就像使用 set -e 一样,这要归功于 ERR 上的 trap 和一些 bash- fu):

有一些额外的功能可以帮助处理错误,例如 try and catchthrow 关键字,可以让您在某个点中断执行以查看回溯。另外,如果终端支持它,它会吐出电力线表情符号,对输出的部分进行着色以提高可读性,并在代码行上下文中强调导致异常的方法。

缺点是 - 它不可移植 - 代码在 bash 中工作,可能仅 >= 4(但我想它可以通过一些努力移植到 bash 3)。

代码被分成多个文件以便更好地处理,但我受到the answer above by Luca Borrione 的回溯想法的启发。

要阅读更多内容或查看源代码,请参阅 GitHub:

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

【讨论】:

这是在 Bash Object Oriented Framework 项目中。 ...幸运的是,它只有 7.4k LOC(根据 GLOC )。 OOP——面向对象的痛苦? @ingyhere 它是高度模块化的(并且易于删除),因此您只能使用例外部分,如果这是您的目的;)【参考方案6】:

我更喜欢真正容易调用的东西。所以我使用了一些看起来有点复杂,但很容易使用的东西。我通常只是将下面的代码复制并粘贴到我的脚本中。代码后面有解释。

#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 函数中调用 cleanup 函数,但这因脚本而异,因此我将其省略了。陷阱捕捉常见的终止信号,并确保一切都得到清理。别名才是真正的魔法。我喜欢检查一切是否失败。所以总的来说,我将程序称为“如果!”类型声明。通过从行号中减去 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

【讨论】:

您能否扩展声明“我们必须明确允许别名”?我担心可能会导致一些意想不到的行为。有没有办法以较小的影响实现相同的目标? 我不需要$LINENO - 1。没有它也能正确显示。 bash 和 zsh 中较短的用法示例 false || die "hello death"【参考方案7】:

另一个考虑因素是要返回的退出代码。只是“1”是相当标准的,尽管有少数reserved exit codes that bash itself uses,并且同一页面认为用户定义的代码应该在 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-ing 代码让您的脚本同时发出多个错误信号。

【讨论】:

【参考方案8】:

我使用以下陷阱代码,它还允许通过管道和“时间”命令跟踪错误

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

【讨论】:

function 关键字与 POSIX 完全不兼容。考虑只声明error() ,前面没有function $$? 应该只是$?,或者$?,如果你坚持使用不必要的大括号;内部$ 是错误的。 @CharlesDuffy 现在,POSIX 与 GNU/Linux 完全不兼容(不过,我同意你的观点)【参考方案9】:

我用过

die() 
        echo $1
        kill $$

之前;我想是因为“退出”出于某种原因对我来说失败了。不过,上述默认值似乎是个好主意。

【讨论】:

最好将错误消息发送到 STDERR,不是吗?【参考方案10】:

This 已经为我服务了一段时间了。它以红色打印错误或警告消息,每个参数一行,并允许可选的退出代码。

# 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

【讨论】:

【参考方案11】:

不确定这是否对您有帮助,但我在此处修改了一些建议的函数,以便在其中包含错误检查(先前命令的退出代码)。 在每次“检查”时,我还将错误的“消息”作为参数传递,以用于记录目的。

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

使用它,我能够为一些自动化过程创建一个非常强大的 bash 文件,它会在出现错误时停止并通知我(log.sh 会这样做)

【讨论】:

考虑使用 POSIX 语法来定义函数 -- 没有 function 关键字,只需 error_exit() 你有理由不只做cd /home/myuser/afolder || error_exit "Unable to switch to folder" 吗? @Pierre-OlivierVares 没有特别的理由不使用 ||。这只是现有代码的摘录,我只是在每个相关行之后添加了“错误处理”行。有些很长,将它放在单独的(直接)行上会更干净 看起来像一个干净的解决方案,但是,shell 检查抱怨:github.com/koalaman/shellcheck/wiki/SC2181【参考方案12】:

这个技巧对于缺少命令或功能很有用。缺少的函数(或可执行文件)的名称将在 $_ 中传递

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

【讨论】:

$_ 不能在与$? 相同的功能中使用吗?我不确定是否有任何理由在函数中使用一个而不是另一个。【参考方案13】:

这个功能最近一直很好地为我服务:

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

如果任何命令失败,错误代码会简单地传递到块的末尾。如果您不希望在之前的命令失败时执行后续命令,但您也不希望脚本立即退出(例如,在循环内),我发现它很有用。

【讨论】:

【参考方案14】:

使用陷阱并不总是一种选择。例如,如果您正在编写某种需要错误处理并且可以从任何脚本调用的可重用函数(在使用辅助函数获取文件之后),则该函数不能假设外部脚本的退出时间,这使得使用陷阱非常困难。使用陷阱的另一个缺点是可组合性差,因为您可能会覆盖之前可能在调用者链中设置的陷阱。

有一个小技巧可以用来在没有陷阱的情况下进行正确的错误处理。正如您可能已经从其他答案中知道的那样,如果您在命令后面使用 || 运算符,set -e 在命令中不起作用,即使您在子shell 中运行它们也是如此;例如,这行不通:

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

但是需要|| 操作符来防止在清理之前从外部函数返回。诀窍是在后台运行内部命令,然后立即等待它。内置的wait 将返回内部命令的退出代码,现在您在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

这里是基于这个想法的通用函数。如果您删除 local 关键字,它应该可以在所有 POSIX 兼容的 shell 中工作,即将所有 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

使用此方法时唯一需要注意的是,从您传递给run 的命令完成的所有Shell 变量修改都不会传播到调用函数,因为该命令在子shell 中运行。

【讨论】:

【参考方案15】:

有时 set -etrap ERRset -o ,set -o pipefailset -o errtrace 无法正常工作,因为它们试图将自动错误检测添加到 shell。这在实践中效果不佳。

在我看来,你应该编写自己的错误检查代码,而不是使用set -e 和其他东西。如果您明智地使用set -e,请注意潜在的陷阱。

为避免在运行代码时出错,您可以使用exec 1&gt;/dev/nullexec 2&gt;/dev/null/dev/null 在 Linux 中是一个空设备文件。这将丢弃任何写入它的内容,并在读取时返回 EOF。您可以在命令末尾使用它

对于try/catch,您可以使用&amp;&amp;|| 来实现类似的行为 使用可以像这样使用&&

 # try

    command &&
    # your command 

 ||  
    # catch exception 

或者你可以使用if else

if [[ Condition ]]; then
    # if true
else
    # if false
fi

$?显示最后一条命令的输出,返回1或0

【讨论】:

以上是关于Bash 中的错误处理的主要内容,如果未能解决你的问题,请参考以下文章

Bash脚本中的错误处理

bash 错误处理 - 文件是不是存在? [复制]

.bat 文件中的错误处理

处理 Terraform 模板文件和 Bash 脚本的解析错误

sh 处理从bash脚本启动的python脚本的错误

处理git bash中使用adb shell时的路径错误问题