Bash中的错误处理

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

他建议在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脚本中是否有更好的错误处理例程?

417327 次浏览

我使用

die() {
echo $1
kill $$
}

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

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

set -e

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

另一个需要考虑的问题是返回的退出码。只有"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-ing代码一起允许你的脚本信号多个同时错误。

使用陷阱!

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退出,并给出一个显式消息。

或者shopt -s extdebug,并对陷阱的第一行进行一些修改,以全面捕获所有非零退出码(注意set -e非错误非零退出码):

error() {
local last_exit_status="$?"
local parent_lineno="$1"
local message="${2:-(no message ($last_exit_status))}"
local code="${3:-$last_exit_status}"
# ... continue as above
}
trap 'error ${LINENO}' ERR
shopt -s extdebug

那么这也是“兼容的”;set -eu

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

#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函数,但这因脚本而异,所以我省略了它。陷阱捕捉常见的终止信号,并确保所有东西都得到清理。化名才是真正的魔力所在。我喜欢检查每件事是否失败。因此,通常我在“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

“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一样)。

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

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

我使用下面的陷阱代码,它也允许通过管道和'time'命令跟踪错误

#!/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
不确定这是否对您有帮助,但我修改了这里的一些建议函数,以便在其中包括错误检查(先前命令的退出代码)。 在每次“检查”中,我还将错误的“消息”作为参数传递给日志记录
#!/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将这样做)

阅读本页上所有的答案启发了我很多
这里是我的提示:

文件内容: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
< p > < br >
用法示例: < br > 文件内容:trap-test.sh

.sh
#!/bin/bash


source 'lib.trap.sh'


echo "doing something wrong now .."
echo "$foo"


exit 0
< p > < br > 运行:< / p >
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!
< p > < br > 从下面的截图中可以看到,输出是彩色的,错误消息是使用的语言。

enter image description here

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

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

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

通过简单地来源库,你可以得到以下内容(即它将在任何错误时停止执行,就像使用set -e一样,这要感谢ERR上的trap和一些bash-fu):

bash-oo-framework error handling

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

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

为了更好地处理,代码被分离到多个文件中,但我受到了以上是卢卡·博里奥内的回答回溯思想的启发。

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

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

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

有一个小技巧可以用来在没有陷阱的情况下进行正确的错误处理。正如你可能已经从其他答案中知道的那样,如果你在命令后使用||操作符,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中工作,即用x=y替换所有local 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中。

这个功能最近对我很有用:

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或最后一个返回值附加到要运行的命令的名称来调用它,这样就可以连接命令,而不必检查错误值。使用this,这个语句块:

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

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

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

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

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

命令的末尾使用它 对于try/catch,你可以使用&&||来实现类似的行为 使用可以使用&&这样的< / p >
{ # try


command &&
# your command


} || {
# catch exception
}

或者你可以使用if else:

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

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