Bash中的错误处理


239

您最喜欢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脚本中使用了更好的错误处理例程?


1
请参阅此详细答案:在Bash脚本中引发错误
codeforester

1
在此处查看日志记录和错误处理实现:github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Answers:


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可变大小写是有意的。全大写仅适用于shell内置函数和环境变量-对其他所有内容使用小写字母可以防止名称空间冲突。另请参见stackoverflow.com/questions/673055/...
查尔斯·达菲

1
在再次破坏之前,请测试您的更改。约定是一件好事,但它们仅次于功能代码。
Draemon

3
@Draemon,我实际上不同意。显然,已损坏的代码已得到注意并已修复。不好的做法,但是大多数都能正常工作的代码可以永远存在(并得到传播)。
查尔斯·达菲

1
但是你没有注意到 由于功能代码是首要考虑的问题,因此代码损坏引起了人们的注意。
Draemon

5
它不是完全免费的(stackoverflow.com/a/10927223/26334),并且如果代码已经与POSIX不兼容,那么删除function关键字并不能使其在POSIX sh下运行,但是我要说的是ve(IMO)通过削弱使用set -e的建议使答案贬值。Stackoverflow与“您的”代码无关,而是与最佳答案有关。
Draemon

123

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

set -e

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


29
set -e并非没有陷阱:请参阅mywiki.wooledge.org/BashFAQ/105
Charles Duffy 2012年

3
@CharlesDuffy,某些陷阱可以通过set -o pipefail
滚刀

7
@CharlesDuffy感谢您指出陷阱;总体而言,我仍然认为set -e收益成本比很高。
布鲁诺·德·弗赖恩

3
@BrunoDeFraine我用set -e我自己,但是irc.freenode.org#bash中的许多其他常规建议(以非常强烈的建议)反对它。至少,有关陷阱应被充分理解。
查尔斯·达菲

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,用于创建良好的bash脚本。
多米尼克·多恩

1
仅供参考- test ${#g_libs[@]} == 0不符合POSIX(POSIX测试支持=字符串比较或-eq数字比较,但不支持==,更不用说POSIX中缺少数组了),如果您希望与POSIX兼容,为什么要您使用的test是世界,而不是数学环境?(( ${#g_libs[@]} == 0 ))毕竟,它更容易阅读。
Charles Duffy 2014年

2
@Luca-这真是太好了!您的照片启发了我创建自己的实现,甚至还走了几步。我已将其张贴在下面的答案中
niieani

3
Bravissimo!这是调试脚本的绝佳方法。古拉爵米尔我说的唯一的事情是这样的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 '19

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

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


1
错误地使用$(foo)裸线而不是仅仅使用foo。为什么通过举个例子来推广它?
查尔斯·达菲

20

受到此处提出的思想的启发,我开发了一种可读且方便的方法来处理bash样板项目中bash脚本中的错误。

通过简单地采购该库,您可以立即获得以下内容(即,它将在出现任何错误时停止执行,就像使用set -etrapon ERR和一些bash-fu一样):

bash-oo-framework错误处理

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

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

该代码被分成多个文件以进行更好的处理,但是我的灵感来自于Luca Borrione的上述回答

要了解更多信息或查看源代码,请参见GitHub:

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


这是在Bash面向对象框架项目中。......幸运的是,它只有7.4k LOC(根据GLOC)。OOP-面向对象的痛苦吗?
ingyhere

@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函数旁边调用清除函数,但这在脚本之间有所不同,因此我将其省略。陷阱捕获常见的终止信号,并确保一切都清理干净。别名是真正的魔术。我喜欢检查一切是否失败。因此,通常我将程序称为“ if!”。类型声明。通过从行号中减去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
您能否扩展一下“我们必须显式允许别名”的说法?我担心会导致某些意外行为。有没有办法以较小的影响实现同一目标?

我不需要$LINENO - 1。没有它就正确显示。
kyb

bash和zsh中更短的用法示例false || die "hello death"
kyb

6

另一个考虑因素是要返回的退出代码。1尽管bash本身使用了一些保留的退出代码,但“ ”是相当标准的,并且同一页还指出,用户定义的代码应在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 2013年

5
${$?}应该是$?,或者${?}如果您坚持使用不必要的括号;内在$是错误的。
Charles Duffy 2013年

3
@CharlesDuffy,到目前为止,POSIX确实与GNU / Linux不兼容(仍然,我
同意

3

我用过

die() {
        echo $1
        kill $$
}

之前; 我认为因为某些原因,“出口”失败了。不过,上述默认设置似乎是个好主意。


最好将err消息发送给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"

使用此程序,我能够为某些自动化过程创建一个真正强大的bash文件,它将在出现错误的情况下停止并通知我(log.sh将执行此操作)


2
考虑使用POSIX语法定义函数-no function关键字,仅error_exit() {
Charles Duffy 2013年

2
你为什么不这样做是有原因的cd /home/myuser/afolder || error_exit "Unable to switch to folder"
Pierre-Olivier Vares 2014年

@ Pierre-OlivierVares没有不使用||的特殊原因。这只是现有代码的摘录,我在每个相关行之后添加了“错误处理”行。有些很长,将它放在单独的(立即)生产线上比较干净
Nelson Rodriguez

外壳检查似乎是一个干净的解决方案,但抱怨: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如果在命令||后使用运算符,即使在子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用just 替换所有关键字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都不会传播到调用函数,因为该命令在子shell中运行。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.