在Bash中处理错误

什么是你最喜欢的方法来处理在Bash中的错误? 处理我在网上发现的错误的最好的例子是由William Shotts,Jr在http://www.linuxcommand.org撰写的。

他build议在Bash中使用以下函数进行error handling:

#!/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脚本中使用的更好的error handling例程吗?

使用陷阱!

 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 

…然后,每当你创build一个临时文件:

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

$temp_foo将在退出时被删除,并且当前行号将被打印。 ( set -e同样会给你错误后退行为, 尽pipe它带有严重的警告 ,削弱了代码的可预测性和可移植性)。

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

 error ${LINENO} "the foobar failed" 2 

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

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

 set -e 

作为一个基本的错误机制。 如果一个简单的命令失败,它会立即停止你的脚本。 我认为这应该是默认行为:因为这样的错误几乎总是意味着一些意想不到的事情,所以继续执行下面的命令并不是一个“理智的”行为。

阅读这个页面上的所有答案激发了我很多。

所以,这是我的提示:

文件内容: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='^([az]{1,}) ([0-9]{1,})$' if [[ $error_lineno =~ $regex ]] # The error line was found on the log # (eg 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 # (eg 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! 

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

在这里输入图像描述

“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 

这排除了在其他响应中提到的正确的error handling,但是快速有效(就像bash)。

受到这里提出的想法的启发,我开发了一个可读和便捷的方法来处理bash脚本中的错误。

通过简单地获取库,你会得到以下的开箱即用(即它会停止执行任何错误,就像使用set -e感谢在ERR和一些bash-fu trap ):

bash-oo框架错误处理

还有一些额外的function可以帮助处理错误,比如try和catch ,或者throw关键字,这些function可以让您在某个点上中断执行以查看回溯。 此外,如果terminal支持它,它将吐出电力线表情符号,为输出的颜色部分着色以提高可读性,并强调在代码行中引起exception的方法。

缺点是 – 它不是可移植的 – 代码在bash中可能工作,大概只有> = 4(但我可以想象它可以移植一些努力bash 3)。

为了更好的处理,代码被分成多个文件,但是我从上面Luca Borrione的回答中得到了回溯思路的启发。

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

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

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

 #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!”中调用程序 types语句。 通过从行号中减去1,别名会告诉我发生故障的位置。 这也很简单,打电话,几乎白痴certificate。 下面是一个例子(只要用/ bin / falsereplace你要调用的任何东西)。

 #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 

另一个考虑是要返回的退出代码。 只有“ 1 ”是非常标准的,尽pipe有一些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代码一起允许脚本发出多个同时发生的错误。

我用过了

 die() { echo $1 kill $$ } 

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

这已经让我好一阵子了。 它以红色打印错误或警告消息,每个参数一行,并允许一个可选的退出代码。

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

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

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

不知道这是否对你有帮助,但是我在这里修改了一些build议的函数,以便在其中包含错误检查(从先前的命令退出代码)。 在每个“检查”我也通过作为参数的错误是为logging目的的“消息”。

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

现在在相同的脚本中调用它(或者在另一个中,如果我使用export -f error_exit ),我只需写下函数的名称并将消息作为parameter passing,如下所示:

 #!/bin/bash cd /home/myuser/afolder error_exit "Unable to switch to folder" rm * error_exit "Unable to delete all files" 

使用这个,我能够创build一个真正强大的bash文件的一些自动化的过程,它会停止在出现错误的情况下,并通知我( log.sh会这样做)

这个function最近一直在服务于我:

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

如果任何命令失败,错误代码将被传递到块的末尾。 当你不希望后面的命令执行,如果一个较早的失败,但你也不希望脚本直接退出(例如,在一个循环内),我觉得它很有用。

您可以使用内置的“调用者”自动将调用的位置提供给“带消息的死亡”function,甚至可以打印整个后退迹线。

你可以从http://jimavera.cixx6.com/Carp.bash下载一些bash函数;

Perl程序员会觉得这些都在家里。

(对不起,我不能把实际的代码放在这篇文章中,因为stackoverflow的网站似乎没有提供任何方法来插入代码,所以它不会被破坏。'pre'标签从“预格式化” “文本,”代码“标签做了一些漂亮的打印,这使得代码不再有效,因为太喜欢它了!

这个技巧对缺less命令或function很有用。 缺less的函数(或可执行文件)的名称将被传入$ _

 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 

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

有一个小窍门,可以用来做正确的error handling没有陷阱。 正如您从其他答案中可能已经知道的那样,如果使用|| ,则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 

|| 在清理之前需要操作员来防止从外部函数返回。 诀窍是在后台运行内部命令,然后立即等待它。 内build的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 

这是build立在这个想法基础上的通用函数。 它应该在所有POSIX兼容的shell中工作,如果你删除local关键字,即用local x=yreplace所有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 

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