Обработка ошибок в Bash
Каков ваш любимый метод обработки ошибок в Bash?
Лучший пример обработки ошибок, которые я нашел в Интернете, написал Уильям Шоттс-младший в 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
Используйте ловушку!
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 и выдаст явное сообщение.
Ответ 2
Это прекрасное решение. Я просто хотел добавить
set -e
как механизм рудиментарной ошибки. Он немедленно остановит ваш script, если простую команду не удастся. Я думаю, что это должно было быть по умолчанию: поскольку такие ошибки почти всегда означают что-то неожиданное, на самом деле не "разумно" выполнять следующие команды.
Ответ 3
Чтение всех ответов на этой странице очень вдохновило меня.
Итак, здесь мой намек:
содержимое файла: 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 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=( "[email protected]" )
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!
Как вы можете видеть на скриншоте ниже, вывод цветной и сообщение об ошибке появляется на используемом языке.
![enter image description here]()
Ответ 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
Вдохновленный представленными здесь идеями, я разработал читаемый и удобный способ обработки ошибок в сценариях bash в моем проекте шаблона bash.
Просто используя библиотеку, вы получите следующее из коробки (то есть она остановит выполнение любой ошибки, как если бы с помощью set -e
благодаря trap
ERR
и некоторым bash-fu):
![bash-oo-framework error handling]()
Есть некоторые дополнительные функции, которые помогают обрабатывать ошибки, такие как try и catch, или ключевое слово throw, которое позволяет вам разбить выполнение в точке, чтобы увидеть обратную трассировку. Кроме того, если терминал поддерживает его, он выплескивает powerline emojis, окрашивает части вывода для большей читаемости и подчеркивает метод, вызвавший исключение в контексте строки кода.
Недостаток - он не переносимый - код работает в bash, возможно> = 4 (но я бы предположил, что его можно портировать с некоторым усилием на bash 3).
Кодекс разделен на несколько файлов для лучшей обработки, но я был вдохновлен идеей backtrace из ответа выше 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 "[email protected]"
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, но она варьируется от script до script, поэтому я ее оставил. Ловушки захватывают общие сигналы о завершении и проверяют, что все очищается. Псевдоним - это то, что делает настоящая магия. Мне нравится проверять все на случай неудачи. Так что вообще я вызываю программы в "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
Еще одно соображение - это код выхода для возврата. Просто "1
" довольно стандартно, хотя есть несколько зарезервированных кодов выхода, которые сам использует bash, и эта же страница утверждает что пользовательские коды должны находиться в диапазоне 64-113, чтобы соответствовать стандартам 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
-связывание кодов вместе позволяет вашему script сигнализировать несколько одновременных ошибок.
Ответ 8
Я использую следующий код ловушки, он также позволяет отслеживать ошибки через каналы и команды времени
#!/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
Ответ 9
Я использовал
die() {
echo $1
kill $$
}
раньше; я думаю, потому что "выход" по какой-то причине терпел неудачу. Однако указанные выше значения по умолчанию выглядят как хорошая идея.
Ответ 10
Это послужило мне хорошо на некоторое время. Он печатает сообщения об ошибках или предупреждения красным цветом, по одной строке для каждого параметра и позволяет получить дополнительный код выхода.
# Custom errors
EX_UNKNOWN=1
warning()
{
# Output warning messages
# Color the output red if it an interactive terminal
# @param $1...: Messages
test -t 1 && tput setf 4
printf '%s\n' "[email protected]" >&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=( "[email protected]" )
# If the last parameter is a number, it 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}
}
Ответ 11
Не уверен, что это будет полезно для вас, но я изменил некоторые из предлагаемых функций здесь, чтобы включить проверку ошибки (код выхода из предыдущей команды) внутри нее.
В каждой "проверке" я также передаю в качестве параметра "сообщение" о том, что ошибка для целей ведения журнала.
#!/bin/bash
error_exit()
{
if [ "$?" != "0" ]; then
log.sh "$1"
exit 1
fi
}
Теперь, чтобы вызвать его в том же script (или в другом, если я использую 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
сделает это)
Ответ 12
В последнее время эта функция хорошо меня обслуживает:
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
"[email protected]"
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 \""[email protected]"\" 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>>>
Если какая-либо из команд выходит из строя, код ошибки просто передается в конец блока. Я считаю это полезным, если вы не хотите, чтобы последующие команды выполнялись, если предыдущий сбой был неудачным, но вы также не хотите, чтобы script немедленно выходил (например, внутри цикла).
Ответ 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
Ответ 14
Использование ловушки не всегда является опцией. Например, если вы пишете какую-то повторно используемую функцию, которая нуждается в обработке ошибок и которая может быть вызвана из любого script (после поиска файла со вспомогательными функциями), эта функция не может принять ничего о времени выхода внешнего script, что затрудняет использование ловушек. Другим недостатком использования ловушек является плохая композиционная способность, так как вы рискуете перезаписать предыдущую ловушку, которая может быть установлена ранее в цепочке звонящих.
Есть небольшой трюк, который можно использовать для правильной обработки ошибок без ловушек. Как вы уже знаете из других ответов, set -e
не работает внутри команд, если после них вы используете оператор ||
, даже если вы запустите их в подоболочке; например, это не сработает:
#!/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
Вот общая функция, которая основывается на этой идее. Он должен работать во всех POSIX-совместимых оболочках, если вы удаляете ключевые слова local
, т.е. Заменяете все 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" "[email protected]" &
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" "[email protected]"
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: [email protected]"
CLEANUP=cleanup run inner "[email protected]"
echo "<-- main"
}
inner() {
echo "--> inner: [email protected]"
sleep 0.5; if [ "$1" = 'fail' ]; then
oh_my_god_look_at_this
fi
echo "<-- inner"
}
cleanup() {
echo "--> cleanup: [email protected]"
echo " RUN_CMD = '$RUN_CMD'"
echo " RUN_EXIT_CODE = $RUN_EXIT_CODE"
sleep 0.3
echo '<-- cleanup'
return $RUN_EXIT_CODE
}
main "[email protected]"
Запуск примера:
$ ./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
, не будут распространяться на вызывающую функцию, потому что команда выполняется в подоболочке.