当前位置:  开发笔记 > 编程语言 > 正文

Bash中的错误处理

如何解决《Bash中的错误处理》经验,为你挑选了7个好方法。

你最喜欢在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 

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> Charles Duff..:

使用陷阱!

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退出,并给出明确的消息.


@draemon可变资本化是有意的.All-caps仅适用于shell内置函数和环境变量 - 使用小写来防止命名空间冲突.另见http://stackoverflow.com/questions/673055/correct-bash-and-shell-script-variable-capitalization/673940#673940
这不是完全无偿的(http://stackoverflow.com/a/10927223/26334)如果代码已经与POSIX不兼容,删除函数关键字并不能让它更能在POSIX下运行,但我的主要观点是你(IMO)通过削弱使用set -e的建议来贬低答案.Stackoverflow不是关于"你的"代码,而是关于获得最佳答案.
@Draemon,我其实不以为然.明显破坏的代码会被注意到并修复.糟糕的做法,但主要是工作代码永远存在(并得到传播).

2> Bruno De Fra..:

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

set -e

作为一种基本的错误机制.如果简单命令失败,它将立即停止您的脚本.我认为这应该是默认行为:因为这样的错误几乎总是表示意外的事情,所以继续执行以下命令并不是真的"理智".


`set -e`并非没有陷阱:请参阅http://mywiki.wooledge.org/BashFAQ/105.
@CharlesDuffy感谢你指着陷阱; 总的来说,我仍然认为`set -e`具有很高的收益 - 成本比率.
@CharlesDuffy,使用`set -o pipefail`可以克服一些问题
@BrunoDeFraine我自己使用`set -e`,但irc.freenode.org中的其他一些常规#bash建议(用非常强烈的语言)反对它.至少,应该很好地理解有问题的陷阱.
设置-e -o pipefail -u#并知道你在做什么
@TomHale,您能正确回答BashFAQ#105中的每个练习(re:`set -e`极端情况)吗?您确定不会遇到https://www.in-ulm.de/~mascheck/various/set-e/中的任何可移植性错误吗?您所说的“安全模式”没有任何“安全”之处。

3> Luca Borrion..:

阅读本页面上的所有答案都给了我很多启发.

所以,这是我的提示:

文件内容: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结合在一起,它创建了一个强大的env来创建好的bash脚本.
@Luca - 这真的很棒!你的图片激发了我创建自己的实现,甚至更进一步.我已将它发布在我的[以下答案]中(http://stackoverflow.com/questions/64786/error-handling-in-bash/30019669#30019669).
Bravissimo!这是调试脚本的绝佳方法.*Grazie mille*我唯一添加的是对OS X的检查,如:Darwin中的`case"$(uname)"stderr_log ="$ {TMPDIR} stderr.log";; Linux)stderr_log ="/ dev/shm/stderr.log";;*)stderr_log ="/ dev/shm/stderr.log";; esac`

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



5> niieani..:

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

通过简单地获取库,您可以获得以下开箱即用(即它将停止执行任何错误,就好像使用set -e感谢trapon ERR和一些bash-fu):

bash-oo-framework错误处理

有一些额外的功能可以帮助处理错误,例如try和catch,或者throw关键字,它允许你在一个点上中断执行以查看回溯.此外,如果终端支持它,它会吐出电源线表情符号,为输出的颜色部分着色以提高可读性,并强调在代码行的上下文中导致异常的方法.

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

代码被分成多个文件以便更好地处理,但我从Luca Borrione的上述答案中得到了回溯的启发.

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

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



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函数的一侧调用清理函数,但这在脚本之间有所不同,所以我把它遗漏了.陷阱捕获常见的终止信号并确保一切都得到清理.别名是真正的魔力.我想检查一切是否失败.所以一般来说我用"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


你能否扩展声明_"我们必须明确允许别名"_?我担心会出现一些意想不到的行为.有没有办法以较小的影响实现同样的目标?

7> yukondude..:

另一个考虑因素是返回的退出代码.只是" 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- 将代码放在一起可让您的脚本发出多个同时发生的错误信号.

推荐阅读
yzh148448
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有