Error handling in Bash
您最喜欢用什么方法来处理bash中的错误?我在网上发现的处理错误的最好例子是由WilliamShotts,Jr在http://www.linuxcommand.org上写的。
他建议在bash中使用以下函数进行错误处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #!/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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 |
…然后,每当创建临时文件时:
1 2 | temp_foo="$(mktemp -t foobar.XXXXXX)" tempfiles+=("$temp_foo" ) |
退出时删除
您可以让陷阱为您调用
1 | error ${LINENO}"the foobar failed" 2 |
将以状态2退出,并给出显式消息。
这是一个很好的解决方案。我只是想补充一下
1 | set -e |
作为一种基本的错误机制。如果一个简单的命令失败,它将立即停止脚本。我认为这应该是默认行为:由于这些错误几乎总是意味着意外的事情,所以继续执行以下命令并不是真正的"理智"。
阅读这一页上的所有答案给了我很多启发。我的提示是:文件内容:lib.trap.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | 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" (!) EXIT HANDLER: " # # 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} " echo -e"ERROR CODE:\t${error_code}" test -t 1 && tput setf 6 ## white yellow echo -e"ERROR MESSAGE: $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 " echo -e"ERROR CODE:\t${error_code}" test -t 1 && tput setf 6 ## white yellow echo -e"ERROR MESSAGE: ${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 " echo -e"ERROR CODE:\t${error_code}" test -t 1 && tput setf 6 ## white yellow if test -n"${stderr}" then echo -e"ERROR MESSAGE: ${stderr}" else echo -e"ERROR MESSAGE: ${error_message}" fi fi fi # # PRINTING THE BACKTRACE: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # test -t 1 && tput setf 7 ## white bold echo -e" $_backtrace " # # 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
1 2 3 4 5 6 7 8 | #!/bin/bash source 'lib.trap.sh' echo"doing something wrong now .." echo"$foo" exit 0 |
运行:
1 | bash trap-test.sh |
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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"的等效替代方案是
1 | set -o errexit |
它使国旗的含义比"-e"更清楚一些。
随机添加:要临时禁用标志并返回默认值(无论退出代码如何,都要继续执行),只需使用
1 2 3 4 5 | 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)。
在这里提出的想法的启发下,我开发了一种可读且方便的方法来处理我的bash样板文件项目中bash脚本中的错误。
通过简单地查找库,您可以得到以下内容(即,如果出现任何错误,它将停止执行,就像由于
有一些额外的功能可以帮助处理错误,例如try and catch或throw关键字,它允许您中断某个点的执行以查看回溯。另外,如果终端支持它,它会吐出电力线emojis,为输出的部分着色以提高可读性,并在代码行上下文中引起异常的方法下加下划线。
缺点是——它不可移植——代码可以在bash中工作,可能只适用于>=4(但我可以想象,它可以通过某种方式移植到bash 3中)。
为了更好的处理,代码被分为多个文件,但是我受到了上面LucaBorrione给出的答案的回溯思想的启发。
要了解更多信息或查看源代码,请参阅GitHub:
https://github.com/niieani/bash oo framework异常错误处理和抛出
我喜欢很容易打电话的。所以我用的东西看起来有点复杂,但很容易使用。我通常只是复制并粘贴下面的代码到我的脚本中。代码后面有一个解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #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 ))`):"' |
我通常在错误退出函数的一侧调用cleanup函数,但这在脚本之间有所不同,所以我忽略了它。陷阱捕捉到常见的终止信号,并确保所有东西都被清理干净。别名是真正的魔法。我喜欢检查每件事是否失败。所以一般来说,我把程序叫做"if!"类型语句。通过从行号中减去1,别名将告诉我失败发生的位置。打电话也很简单,而且很容易证明自己是白痴。下面是一个例子(只需用您要调用的内容替换/bin/false)。
1 2 3 4 5 | #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 |
另一个考虑因素是要返回的退出代码。仅仅是"EDOCX1·0"是相当标准的,虽然有少量保留的退出代码,BASH自己使用,并且同一页认为用户定义的代码应该在范围64-113,以符合C/C++标准。
您还可以考虑
1 2 3 4 5 6 7 8 | 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 |
我使用以下陷阱代码,它还允许通过管道和"time"命令跟踪错误
1 2 3 4 5 6 7 8 9 10 11 | #!/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 |
这对我很有帮助。它以红色打印错误或警告消息,每个参数一行,并允许可选的退出代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | # 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 '"$@">&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} } |
我已经用过
1 2 3 4 | die() { echo $1 kill $$ } |
以前;我想是因为"退出"对我来说是因为某种原因而失败的。不过,上述违约似乎是个好主意。
不确定这是否对您有帮助,但是我修改了这里的一些建议函数,以便在其中包含对错误的检查(从前面的命令中退出代码)。在每一个"检查"中,我还将错误的"消息"作为一个参数传递给日志记录。
1 2 3 4 5 6 7 8 9 | #!/bin/bash error_exit() { if ["$?" !="0" ]; then log.sh"$1" exit 1 fi } |
现在,要在同一个脚本中调用它(或者在另一个脚本中调用,如果我使用
1 2 3 4 5 6 7 | #!/bin/bash cd /home/myuser/afolder error_exit"Unable to switch to folder" rm * error_exit"Unable to delete all files" |
使用这个工具,我可以为一些自动化进程创建一个真正健壮的bash文件,如果出现错误,它将停止并通知我(
这项职能最近对我很有帮助:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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或最后一个返回值来调用它,这样就可以在不检查错误值的情况下链接命令。使用此语句块:
1 2 3 4 5 6 | command1 param1 param2 param3... command2 param1 param2 param3... command3 param1 param2 param3... command4 param1 param2 param3... command5 param1 param2 param3... command6 param1 param2 param3... |
变成这样:
1 2 3 4 5 6 7 8 | 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>>> |
如果任何一个命令失败,错误代码将简单地传递到块的末尾。当您不希望在早期命令失败时执行后续命令,但也不希望脚本立即退出(例如,在循环中)时,我发现它很有用。
使用陷阱并不总是一种选择。例如,如果您正在编写某种需要错误处理的可重用函数,并且可以从任何脚本调用该函数(在使用辅助函数获取文件之后),则该函数不能假定外部脚本的退出时间,这使得使用陷阱非常困难。使用陷阱的另一个缺点是不好的可组合性,因为您可能会覆盖以前在调用链中设置的陷阱。
有一个小技巧可以用来在没有陷阱的情况下进行正确的错误处理。正如您从其他答案中可能已经知道的那样,如果在命令之后使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #!/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 |
但是需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #!/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 |
这里是基于这个想法的通用函数。如果删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | # [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 } |
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #!/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"$@" |
运行示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ ./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 |
在使用此方法时,您需要注意的唯一一件事是,从传递给
这个技巧对于缺少命令或函数很有用。将传入缺少的函数(或可执行文件)的名称$_
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 |