Catch error linux

Написание надежного, без ошибок сценария bash всегда является сложной задачей. Даже если вы написать идеальный сценарий bash, он все равно может не

Написание надежного, без ошибок сценария bash всегда является сложной задачей. Даже если вы написать идеальный сценарий bash, он все равно может не сработать из-за внешних факторов, таких как некорректный ввод или проблемы с сетью.

В оболочке bash нет никакого механизма поглощения исключений, такого как конструкции try/catch. Некоторые ошибки bash могут быть молча проигнорированы, но могут иметь последствия в дальнейшем. Перехват и обработка ошибок в bash

Проверка статуса завершения команды

Всегда рекомендуется проверять статус завершения команды, так как ненулевой статус выхода обычно указывает на ошибку

if ! command; then
    echo "command returned an error"
fi

Другой (более компактный) способ инициировать обработку ошибок на основе статуса выхода — использовать OR:

<command_1> || <command_2>

С помощью оператора OR, <command_2> выполняется тогда и только тогда, когда <command_1> возвращает ненулевой статус выхода.

В качестве второй команды, можно использовать свою Bash функцию обработки ошибок

error_exit()
{
    echo "Error: $1"
    exit 1
}

bad-command || error_exit "Some error"

В Bash имеется встроенная переменная $?, которая сообщает вам статус выхода последней выполненной команды.

Когда вызывается функция bash, $? считывает статус выхода последней команды, вызванной внутри функции. Поскольку некоторые ненулевые коды выхода имеют специальные значения, вы можете обрабатывать их выборочно.

status=$?

case "$status" in
"1") echo "General error";;
"2") echo "Misuse of shell builtins";;
"126") echo "Command invoked cannot execute";;
"128") echo "Invalid argument";;
esac

Выход из сценария при ошибке в Bash

Когда возникает ошибка в сценарии bash, по умолчанию он выводит сообщение об ошибке в stderr, но продолжает выполнение в остальной части сценария. Даже если ввести неправильную команду, это не приведет к завершению работы сценария. Вы просто увидите ошибку «command not found».

Такое поведение оболочки по умолчанию может быть нежелательным для некоторых bash сценариев. Например, если скрипт содержит критический блок кода, в котором не допускаются ошибки, вы хотите, чтобы ваш скрипт немедленно завершал работу при возникновении любой ошибки внутри этого блока . Чтобы активировать это поведение «выход при ошибке» в bash, вы можете использовать команду set следующим образом.

set -e
# некоторый критический блок кода, где ошибка недопустима
set +e

Вызванная с опцией -e, команда set заставляет оболочку bash немедленно завершить работу, если любая последующая команда завершается с ненулевым статусом (вызванным состоянием ошибки). Опция +e возвращает оболочку в режим по умолчанию. set -e эквивалентна set -o errexit. Аналогично, set +e является сокращением команды set +o errexit.

set -e
true | false | true
echo "Это будет напечатано" # "false" внутри конвейера не обнаружено

Если необходимо, чтобы при любом сбое в работе конвейеров также завершался сценарий bash, необходимо добавить опцию -o pipefail.

set -o pipefail -e
true | false | true # "false" внутри конвейера определен правильно
echo "Это не будет напечатано"

Для «защиты» критический блока в сценарии от любого типов ошибок команд или ошибок конвейера, необходимо использовать следующую комбинацию команд set.

set -o pipefail -e
# некоторый критический блок кода, в котором не допускается ошибка или ошибка конвейера
set +o pipefail +e

To expand on the @Gilles’ answer:

Indeed, set -e doesn’t work inside commands if you use || operator after them, even if you run them in a subshell; e.g., this wouldn’t work:

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

But || operator is needed to prevent returning from the outer function before cleanup.

There is a little trick that can be used to fix this: run the inner command in background, and then immediately wait for it. The wait builtin will return the exit code of the inner command, and now you’re using || after wait, not the inner function, so set -e works properly inside the latter:

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

Here is the generic function that builds upon this idea. It should work in all POSIX-compatible shells if you remove local keywords, i.e. replace all local x=y with just 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
}

Example of usage:

#!/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 "$@"

Running the example:

$ ./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

The only thing that you need to be aware of when using this method is that all modifications of Shell variables done from the command you pass to run will not propagate to the calling function, because the command runs in a subshell.

catch: Evaluate script and trap exceptional returns

Command to display catch manual in Linux: $ man n catch

NAME

catch — Evaluate script and trap exceptional returns

SYNOPSIS

catch script ?resultVarName? ?optionsVarName?

DESCRIPTION

The catch command may be used to prevent errors from aborting command
interpretation. The catch command calls the Tcl interpreter recursively to
execute script, and always returns without raising an error,
regardless of any errors that might occur while executing script.

If script raises an error, catch will return a non-zero integer
value corresponding to the exceptional return code returned by evaluation
of script. Tcl defines the normal return code from script
evaluation to be zero (0), or TCL_OK. Tcl also defines four exceptional
return codes: 1 (TCL_ERROR), 2 (TCL_RETURN), 3 (TCL_BREAK),
and 4 (TCL_CONTINUE). Errors during evaluation of a script are indicated
by a return code of TCL_ERROR. The other exceptional return codes are
returned by the return, break, and continue commands
and in other special situations as documented. Tcl packages can define
new commands that return other integer values as return codes as well,
and scripts that make use of the return -code command can also
have return codes other than the five defined by Tcl.

If the resultVarName argument is given, then the variable it names is
set to the result of the script evaluation. When the return code from
the script is 1 (TCL_ERROR), the value stored in resultVarName is an error
message. When the return code from the script is 0 (TCL_OK), the value
stored in resultVarName is the value returned from script.

If the optionsVarName argument is given, then the variable it
names is set to a dictionary of return options returned by evaluation
of script. Tcl specifies two entries that are always
defined in the dictionary: -code and -level. When
the return code from evaluation of script is not TCL_RETURN,
the value of the -level entry will be 0, and the value
of the -code entry will be the same as the return code.
Only when the return code is TCL_RETURN will the values of
the -level and -code entries be something else, as
further described in the documentation for the return command.

When the return code from evaluation of script is TCL_ERROR,
three additional entries are defined in the dictionary of return options
stored in optionsVarName: -errorinfo, -errorcode,
and -errorline. The value of the -errorinfo entry
is a formatted stack trace containing more information about
the context in which the error happened. The formatted stack
trace is meant to be read by a person. The value of
the -errorcode entry is additional information about the
error stored as a list. The -errorcode value is meant to
be further processed by programs, and may not be particularly
readable by people. The value of the -errorline entry
is an integer indicating which line of script was being
evaluated when the error occurred. The values of the -errorinfo
and -errorcode entries of the most recent error are also
available as values of the global variables ::errorInfo
and ::errorCode respectively.

Tcl packages may provide commands that set other entries in the
dictionary of return options, and the return command may be
used by scripts to set return options in addition to those defined
above.

EXAMPLES

The catch command may be used in an if to branch based on
the success of a script.

if { [catch {open $someFile w} fid] } {
    puts stderr "Could not open $someFile for writingn$fid"
    exit 1
}

There are more complex examples of catch usage in the
documentation for the return command.

KEYWORDS

catch, error

Bash «try catch»


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters

Show hidden characters

#!/bin/bash
export AnException=100
export AnotherException=101
# start with a try
try
( # open a subshell !!!
echo «do something«
[ someErrorCondition ] && throw $AnException
echo «do something more«
executeCommandThatMightFail || throw $AnotherException
throwErrors # automaticatly end the try block, if command-result is non-null
echo «now on to something completely different«
executeCommandThatMightFail
echo «it’s a wonder we came so far«
executeCommandThatFailsForSure || true # ignore a single failing command
ignoreErrors # ignore failures of commands until further notice
executeCommand1ThatFailsForSure
local result = $(executeCommand2ThatFailsForSure)
[ result != «expected error« ] && throw $AnException # ok, if it’s not an expected error, we want to bail out!
executeCommand3ThatFailsForSure
echo «finished«
)
# directly after closing the subshell you need to connect a group to the catch using ||
catch || {
# now you can handle
case $ex_code in
$AnException)
echo «AnException was thrown«
;;
$AnotherException)
echo «AnotherException was thrown«
;;
*)
echo «An unexpected exception was thrown«
throw $ex_code # you can rethrow the «exception» causing the script to exit if not caught
;;
esac
}


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters

Show hidden characters

#!/bin/bash
function try()
{
[[ $- = *e* ]]; SAVED_OPT_E=$?
set +e
}
function throw()
{
exit $1
}
function catch()
{
export ex_code=$?
(( $SAVED_OPT_E )) && set +e
return $ex_code
}
function throwErrors()
{
set -e
}
function ignoreErrors()
{
set +e
}

Когда вы используете Try/Catch/Finally, команда которая будет выполняться помещается в блок Try. Если произойдет ошибка в процессе выполнения команды, то она будет записана в переменную $Error, и выполнение скрипта перейдет к блоку Catch.

Скрипт TestTryCatchFinally.ps1 использует команду Try в попытке создать объект. Объект создания находится в переменной $ob1. Командлет New-Object создает объект. После создания объекта и помещения его в переменную $a можно посмотреть члены объекта, используя командлет Get-Member. Следующий код иллюстрирует это:

Try
 {
  "Attempting to create new object $ob1"
   $a = new-object $ob1
   "Members of the $ob1"
   "New object $ob1 created"
   $a | Get-Member
 }

Используйте блок Catch чтобы поймать ошибку, которая произошла в блоке Try. Вы можете указать тип ошибок для захвата, а также действие, которое бы происходило при возникновении ошибки. В скрипте TestTryCatchFinally.ps1 я слежу за ошибками типа System.Exception. Класс System.Exeption .Net Framework — это базовый класс, от которого зависят все другие исключения (exceptions). Это означает, что System.Exeption является универсальным общим классом, в сущности вы можете получить все предопределенные исключения как общие, так и системные. При перехвате ошибки вы можете указать какой код вы хотели бы выполнить. В данном примере я вывожу простую строку, которая показывает, что скрипт поймал системное исключение. Блок Catch показан ниже:

Catch
 {
  [system.exception]
  "caught a system exception"
 }

Блок Finally последовательности Try/Catch/Finally всегда выполняется, независимо от того произошла ошибка или нет. Это означает что какие-то вещи для завершения, которые вы хотите сделать, например для явного освобождения COM объекта, следует помещать в блок Finally. В скрипте TestTryCatchFinally.ps1 блок Finally иллюстрирует строку состояния, что скрипт завершен. Это показано далее:

Finally
 {
  "end of script"
 }

Целиком скрипт TestTryCatchFinally.ps1:

TestTryCatchFinally.ps1

$ob1 = "kenobie"
"Begin test"

Try
 {
  "Attempting to create new object $ob1"
   $a = new-object $ob1
   "Members of the $ob1"
   "New object $ob1 created"
   $a | Get-Member
 }
Catch [system.exception]
 {
  "caught a system exception"
 }
Finally
 {
  "end of script"
 }

Во время исполнения сценария TestTryCatchFinally.ps1 когда переменной $ob1 присваивается значение «kenobie» ошибка возникает потому что нет ни одного объекта с именем «kenobie», который можно было бы создать с помощью командлета New-Object. Следующая картинка демонстрирует вывод сценария.

Как видно из предыдущей картинки, строка «Begin Test» выводится потому что находится за пределами цикла Try/Catch/Finally. Внутри блока Try строка “Attempting to create new object kenobie” выводится потому что, выполняется перед командой New-Object. Это демонстрирует то, что блок Try всегда пытается выполнить код внутри него. Члены объекта «kenobie» не выводятся, также как не выводится строка «new object kenobie created». Это указывает на то, что после возникновения ошибки сценарий переходит к следующему блоку. В блоке Catch происходит захват ошибки типа System.Exeption и выводится строка «caught a system exception». Далее скрипт переходит к блоку Finally и выводит строку «end of script».

Если в сценарии переменной $ob1 будет присвоено значение «system.object» (которое является корректным значением), блок Try успешно выполнится полностью. Это видно на следующем рисунке, члены объекты выведены, и строка указывающая чтоб объект успешно создан, также выводится. Блок Catch не срабатывает, а строка «end of script» из блока Finally выводится.

Вы можете использовать несколько блоков Catch в блоке Try/Catch/Finally. Нужно иметь в виду, что когда происходит исключение Windows Powershell покидает блок Try и ищет блок Catch. Будет использован первый блок Catch, который удовлетворит условиям исключения. Таким образом необходимо использовать наиболее конкретные исключения сначала, переходя к более общим.
Это видно в TestTryMultipleCatchFinally.ps1.

TestTryMultipleCatchFinally.ps1

$ob1 = "foo"
"Begin test"
$ErrorActionPreference = "stop"
Try
 {
  Get-Content foo
  "Attempting to create new object $ob1"
   $a = new-object $ob1
   "Members of the $ob1"
   "New object $ob1 created"
   $a | Get-Member
 }
Catch [System.Management.Automation.PSArgumentException]
 {
  "invalid object"
 }
Catch [system.exception]
 {
  "caught a system exception"
 }
Finally
 {
  "end of script"
 }

Следующий рисунок показывает вывод сценария TestTryMultipleCatchFinally.ps1. Были сделаны два изменения: Закомментированы команды $ErrorActionPreference и Get-Content foo. Таким образом генерируемая ошибка будет возникать при попытке создать несуществующий объект. Чтобы найти конкретную ошибку, я исследовал переменную $error после запуска сценария TestTryMultipleCatchFinally.ps1. Ошибка указана в поле Exception.

PS C:> $error | fl * -F

PSMessageDetails      : 
Exception             : System.Management.Automation.PSArgumentException: Cannot find type
                         [foo]: verify that the assembly containing this type is loaded.
                           at System.Management.Automation.MshCommandRuntime.ThrowTerminat
                        ingError(ErrorRecord errorRecord)
TargetObject          : 
CategoryInfo          : InvalidType: (:) [New-Object], PSArgumentException
FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand
ErrorDetails          : 
InvocationInfo        : System.Management.Automation.InvocationInfo
ScriptStackTrace      : at <ScriptBlock>, C:UsersdyakSkyDriveScriptshabrahabrTestTry
                        MultipleCatchFinally.ps1: line 10
PipelineIterationInfo : {}

Если скрипт имеет несколько ошибок, а значение переменной $ErroActionPreference установлено в значение «stop», первая ошибка приведет к сбою сценария. Если убрать комментарии с команд $ErrorActionPreference и Get-Content, первая ошибка будет поймана блоком System.Exception Catch и поэтому будет пропущено исключение аргумента. Это видно на следующем рисунке:

В целом пытался перевести статью опубликованную в блоке Эда Уилсона. Надеюсь будет кому-то полезно.

I have a large iptables ruleset that I manage with my own bash script. Most of the commands in the script are simple, single-statment iptables commands. I am trying to improve the script by adding success/failure output as the script executes.

I have the script broken out into different sections. One example would be the FORWARD chain section, where all the rules are applied to the FORWARD chain. At the beginning of the section, I output that the script has started applying the FORWARD rules, and at the end, I want to output whether or not all the rules were applied successfully, or if any of them didn’t work. Here is the basic idea:

#Start FORWARD section
echo -ne "Applying FORWARD rules..."

#rule 1
/sbin/iptables -A FOWRARD...

#rule 2
/sbin/iptables -A FORWARD...

echo -ne "ttt[OK]n"

What I’m wanting to do is catch any output or errors that may result from each iptables command and store them in an array or something. Then at the end of the block, use an if statement to evaluate the array to see if there were any errors. If not, output the [OK] status, if there were, output the [FAILED] status and display the related error.

Is there a way I can do this for the entire block of rules without wrapping each iptables rule in an if [ $? != 0 ] expression?

Contents

  • 1 Problem
  • 2 Solutions
    • 2.1 Executed in subshell, exit on error
    • 2.2 Executed in subshell, trap error
  • 3 Caveat 1: `Exit on error’ ignoring subshell exit status
    • 3.1 Solution: Generate error yourself if subshell fails
      • 3.1.1 Example 1
      • 3.1.2 Example 2
  • 4 Caveat 2: `Exit on error’ not exitting subshell on error
    • 4.1 Solution: Use logical operators (&&, ||) within subshell
      • 4.1.1 Example
  • 5 Caveat 3: `Exit on error’ not exitting command substition on error
    • 5.1 Solution 1: Use logical operators (&&, ||) within command substitution
    • 5.2 Solution 2: Enable posix mode
  • 6 The tools
    • 6.1 Exit on error
      • 6.1.1 Specify `bash -e’ as the shebang interpreter
        • 6.1.1.1 Example
      • 6.1.2 Set ERR trap to exit
        • 6.1.2.1 Example
  • 7 Solutions revisited: Combining the tools
    • 7.1 Executed in subshell, trap on exit
      • 7.1.1 Rationale
    • 7.2 Sourced in current shell
      • 7.2.1 Todo
      • 7.2.2 Rationale
        • 7.2.2.1 `Exit’ trap in sourced script
        • 7.2.2.2 `Break’ trap in sourced script
        • 7.2.2.3 Trap in function in sourced script without `errtrace’
        • 7.2.2.4 Trap in function in sourced script with ‘errtrace’
        • 7.2.2.5 `Break’ trap in function in sourced script with `errtrace’
  • 8 Test
  • 9 See also
  • 10 Journal
    • 10.1 20210114
    • 10.2 20060524
    • 10.3 20060525
  • 11 Comments

Problem

I want to catch errors in bash script using set -e (or set -o errexit or trap ERR). What are best practices?

Solutions

See #Solutions revisited: Combining the tools for detailed explanations.

If the script is executed in a subshell, it’s relative easy: You don’t have to worry about backing up and restoring shell options and shell traps, because they’re automatically restored when you exit the subshell.

Executed in subshell, exit on error

Example script:

#!/bin/bash -eu
# -e: Exit immediately if a command exits with a non-zero status.
# -u: Treat unset variables as an error when substituting.
 
(false)                   # Caveat 1: If an error occurs in a subshell, it isn't detected
(false) || false          # Solution: If you want to exit, you have to detect the error yourself
(false; true) || false    # Caveat 2: The return status of the ';' separated list is `true'
(false && true) || false  # Solution: If you want to control the last command executed, use `&&'

See also #Caveat 1: `Exit on error’ ignoring subshell exit status

Executed in subshell, trap error

#!/bin/bash -Eu
# -E: ERR trap is inherited by shell functions.
# -u: Treat unset variables as an error when substituting.
# 
# Example script for handling bash errors.  Exit on error.  Trap exit.
# This script is supposed to run in a subshell.
# See also: http://fvue.nl/wiki/Bash:_Error_handling

    #  Trap non-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM, ERR
trap onexit 1 2 3 15 ERR


#--- onexit() -----------------------------------------------------
#  @param $1 integer  (optional) Exit status.  If not set, use `$?'

function onexit() {
    local exit_status=${1:-$?}
    echo Exiting $0 with $exit_status
    exit $exit_status
}


# myscript


    # Allways call `onexit' at end of script
onexit

Caveat 1: `Exit on error’ ignoring subshell exit status

The `-e’ setting does not exit if an error occurs within a subshell, for example with these subshell commands: (false) or bash -c false

Example script caveat1.sh:

#!/bin/bash -e
echo begin
(false)
echo end

Executing the script above gives:

$ ./caveat1.sh
begin
end
$

Conclusion: the script didn’t exit after (false).

Solution: Generate error yourself if subshell fails

( SHELL COMMANDS ) || false

In the line above, the exit status of the subshell is checked. The subshell must exit with a zero status — indicating success, otherwise `false’ will run, generating an error in the current shell.

Note that within a bash `list’, with commands separated by a `;’, the return status is the exit status of the last command executed. Use the control operators `&&’ and `||’ if you want to control the last command executed:

$ (false; true) || echo foo
$ (false && true) || echo foo
foo
$

Example 1

Example script example.sh:

#!/bin/bash -e
echo begin
(false) || false
echo end

Executing the script above gives:

$ ./example.sh
begin
$

Conclusion: the script exits after false.

Example 2

Example bash commands:

$ trap 'echo error' ERR       # Set ERR trap
$ false                       # Non-zero exit status will be trapped
error
$ (false)                     # Non-zero exit status within subshell will not be trapped
$ (false) || false            # Solution: generate error yourself if subshell fails
error
$ trap - ERR                  # Reset ERR trap

Caveat 2: `Exit on error’ not exitting subshell on error

The `-e’ setting doesn’t always immediately exit the subshell `(…)’ when an error occurs. It appears the subshell behaves as a simple command and has the same restrictions as `-e’:

Exit immediately if a simple command exits with a non-zero status, unless the subshell is part of the command list immediately following a `while’ or `until’ keyword, part of the test in an `if’ statement, part of the right-hand-side of a `&&’ or `||’ list, or if the command’s return status is being inverted using `!’

Example script caveat2.sh:

#!/bin/bash -e
(false; echo A)                        # Subshell exits after `false'
!(false; echo B)                       # Subshell doesn't exit after `false'
true && (false; echo C)                # Subshell exits after `false'
(false; echo D) && true                # Subshell doesn't exit after `false'
(false; echo E) || false               # Subshell doesn't exit after `false'
if (false; echo F); then true; fi      # Subshell doesn't exit after `false'
while (false; echo G); do break; done  # Subshell doesn't exit after `false'
until (false; echo H); do break; done  # Subshell doesn't exit after `false'

Executing the script above gives:

$ ./caveat2.sh
B
D
E
F
G
H

Solution: Use logical operators (&&, ||) within subshell

Use logical operators `&&’ or `||’ to control execution of commands within a subshell.

Example

#!/bin/bash -e
(false && echo A)
!(false && echo B)
true && (false && echo C)
(false && echo D) && true
(false && echo E) || false
if (false && echo F); then true; fi
while (false && echo G); do break; done
until (false && echo H); do break; done

Executing the script above gives no output:

$ ./example.sh
$

Conclusion: the subshells do not output anything because the `&&’ operator is used instead of the command separator `;’ as in caveat2.sh.

Caveat 3: `Exit on error’ not exitting command substition on error

The `-e’ setting doesn’t immediately exit command substitution when an error occurs, except when bash is in posix mode:

$ set -e
$ echo $(false; echo A)
A

Solution 1: Use logical operators (&&, ||) within command substitution

$ set -e
$ echo $(false || echo A)

Solution 2: Enable posix mode

When posix mode is enabled via set -o posix, command substition will exit if `-e’ has been set in the
parent shell.

$ set -e
$ set -o posix
$ echo $(false; echo A)

Enabling posix might have other effects though?

The tools

Exit on error

Bash can be told to exit immediately if a command fails. From the bash manual («set -e»):

«Exit immediately if a simple command (see SHELL GRAMMAR above) exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a && or || list, or if the command’s return value is being inverted via !. A trap on ERR, if set, is executed before the shell exits.»

To let bash exit on error, different notations can be used:

  1. Specify `bash -e’ as shebang interpreter
  2. Start shell script with `bash -e’
  3. Use `set -e’ in shell script
  4. Use `set -o errexit’ in shell script
  5. Use `trap exit ERR’ in shell script

Specify `bash -e’ as the shebang interpreter

You can add `-e’ to the shebang line, the first line of your shell script:

#!/bin/bash -e

This will execute the shell script with `-e’ active. Note `-e’ can be overridden by invoking bash explicitly (without `-e’):

$ bash shell_script
Example

Create this shell script example.sh and make it executable with chmod u+x example.sh:

#!/bin/bash -e
echo begin
false     # This should exit bash because `false' returns error
echo end  # This should never be reached

Example run:

$ ./example.sh
begin
$ bash example.sh
begin
end
$

Set ERR trap to exit

By setting an ERR trap you can catch errors as well:

trap command ERR

By setting the command to `exit’, bash exits if an error occurs.

trap exit ERR
Example

Example script example.sh

#!/bin/bash
trap exit ERR
echo begin
false
echo end

Example run:

$ ./example.sh
begin
$

The non-zero exit status of `false’ is catched by the error trap. The error trap exits and `echo end’ is never reached.

Solutions revisited: Combining the tools

Executed in subshell, trap on exit

#!/bin/bash
# --- subshell_trap.sh -------------------------------------------------
# Example script for handling bash errors.  Exit on error.  Trap exit.
# This script is supposed to run in a subshell.
# See also: http://fvue.nl/wiki/Bash:_Error_handling
 
    # Let shell functions inherit ERR trap.  Same as `set -E'.
set -o errtrace 
    # Trigger error when expanding unset variables.  Same as `set -u'.
set -o nounset
    #  Trap non-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM, ERR
    #  NOTE1: - 9/KILL cannot be trapped.
    #+        - 0/EXIT isn't trapped because:
    #+          - with ERR trap defined, trap would be called twice on error
    #+          - with ERR trap defined, syntax errors exit with status 0, not 2
    #  NOTE2: Setting ERR trap does implicit `set -o errexit' or `set -e'.
trap onexit 1 2 3 15 ERR
 
 
#--- onexit() -----------------------------------------------------
#  @param $1 integer  (optional) Exit status.  If not set, use `$?'
 
function onexit() {
    local exit_status=${1:-$?}
    echo Exiting $0 with $exit_status
    exit $exit_status
}
 
 
 
# myscript
 
 
 
    # Allways call `onexit' at end of script
onexit

Rationale

+-------+   +----------+  +--------+  +------+
| shell |   | subshell |  | script |  | trap |
+-------+   +----------+  +--------+  +------+
     :           :            :           :
    +-+         +-+          +-+  error  +-+
    | |         | |          | |-------->| |
    | |  exit   | |          | !         | |
    | |<-----------------------------------+
    +-+          :            :           :
     :           :            :           :

Figure 1. Trap in executed script
When a script is executed from a shell, bash will create a subshell in which the script is run. If a trap catches an error, and the trap says `exit’, this will cause the subshell to exit.

Sourced in current shell

If the script is sourced (included) in the current shell, you have to worry about restoring shell options and shell traps. If they aren’t restored, they might cause problems in other programs which rely on specific settings.

#!/bin/bash
#--- listing6.inc.sh ---------------------------------------------------
# Demonstration of ERR trap being reset by foo_deinit() with the use
# of `errtrace'.
# Example run:
#
#    $ set +o errtrace         # Make sure errtrace is not set (bash default)
#    $ trap - ERR              # Make sure no ERR trap is set (bash default)
#    $ . listing6.inc.sh       # Source listing6.inc.sh
#    $ foo                     # Run foo()
#    foo_init
#    Entered `trap-loop'
#    trapped
#    This is always executed - with or without a trap occurring
#    foo_deinit
#    $ trap                    # Check if ERR trap is reset.
#    $ set -o | grep errtrace  # Check if the `errtrace' setting is...
#    errtrace        off        # ...restored.
#    $
#
# See: http://fvue.nl/wiki/Bash:_Error_handling
 
function foo_init {
    echo foo_init 
    fooOldErrtrace=$(set +o | grep errtrace)
    set -o errtrace
    trap 'echo trapped; break' ERR   # Set ERR trap 
}
function foo_deinit {
    echo foo_deinit
    trap - ERR                # Reset ERR trap
    eval $fooOldErrtrace      # Restore `errtrace' setting
    unset fooOldErrtrace      # Delete global variable
}
function foo {
    foo_init
        # `trap-loop'
    while true; do
        echo Entered `trap-loop'
        false
        echo This should never be reached because the `false' above is trapped
        break
    done
    echo This is always executed - with or without a trap occurring
    foo_deinit
}

Todo

  • an existing ERR trap must be restored and called
  • test if the `trap-loop’ is reached if the script breaks from a nested loop

Rationale

`Exit’ trap in sourced script

When the script is sourced in the current shell, it’s not possible to use `exit’ to terminate the program: This would terminate the current shell as well, as shown in the picture underneath.

+-------+                 +--------+  +------+
| shell |                 | script |  | trap |
+-------+                 +--------+  +------+
    :                         :           :
   +-+                       +-+  error  +-+
   | |                       | |-------->| |
   | |                       | |         | |
   | | exit                  | |         | |
<------------------------------------------+
    :                         :           :

Figure 2. `Exit’ trap in sourced script
When a script is sourced from a shell, bash will run the script in the current shell. If a trap catches an error, and the trap says `exit’, this will cause the current shell to exit.

`Break’ trap in sourced script

A solution is to introduce a main loop in the program, which is terminated by a `break’ statement within the trap.

+-------+    +--------+  +--------+   +------+
| shell |    | script |  | `loop' |   | trap |
+-------+    +--------+  +--------+   +------+
     :           :            :          :  
    +-+         +-+          +-+  error +-+
    | |         | |          | |------->| |
    | |         | |          | |        | |
    | |         | |  break   | |        | |
    | |  return | |<----------------------+
    | |<----------+           :          :
    +-+          :            :          :
     :           :            :          :

Figure 3. `Break’ trap in sourced script
When a script is sourced from a shell, e.g. . ./script, bash will run the script in the current shell. If a trap catches an error, and the trap says `break’, this will cause the `loop’ to break and to return to the script.

For example:

#!/bin/bash
#--- listing3.sh -------------------------------------------------------
# See: http://fvue.nl/wiki/Bash:_Error_handling

trap 'echo trapped; break' ERR;  # Set ERR trap

function foo { echo foo; false; }  # foo() exits with error

    # `trap-loop'
while true; do
    echo Entered `trap-loop'
    foo
    echo This is never reached
    break
done

echo This is always executed - with or without a trap occurring

trap - ERR  # Reset ERR trap

Listing 3. `Break’ trap in sourced script
When a script is sourced from a shell, e.g. ./script, bash will run the script in the current shell. If a trap catches an error, and the trap says `break’, this will cause the `loop’ to break and to return to the script.

Example output:

$> source listing3.sh
Entered `trap-loop'
foo
trapped
This is always executed after a trap
$>
Trap in function in sourced script without `errtrace’

A problem arises when the trap is reset from within a function of a sourced script. From the bash manual, set -o errtrace or set -E:

If set, any trap on `ERR’ is inherited by shell functions, command

substitutions, and commands executed in a subshell environment.

The `ERR’ trap is normally not inherited in such cases.

So with errtrace not set, a function does not know of any `ERR’ trap set, and thus the function is unable to reset the `ERR’ trap. For example, see listing 4 underneath.

#!/bin/bash
#--- listing4.inc.sh ---------------------------------------------------
# Demonstration of ERR trap not being reset by foo_deinit()
# Example run:
# 
#    $> set +o errtrace     # Make sure errtrace is not set (bash default)
#    $> trap - ERR          # Make sure no ERR trap is set (bash default)
#    $> . listing4.inc.sh   # Source listing4.inc.sh
#    $> foo                 # Run foo()
#    foo_init
#    foo
#    foo_deinit             # This should've reset the ERR trap...
#    $> trap                # but the ERR trap is still there:
#    trap -- 'echo trapped' ERR
#    $> trap

# See: http://fvue.nl/wiki/Bash:_Error_handling

function foo_init   { echo foo_init 
                      trap 'echo trapped' ERR;} # Set ERR trap 

function foo_deinit { echo foo_deinit
                      trap - ERR             ;} # Reset ERR trap

function foo        { foo_init
                      echo foo
                      foo_deinit             ;}

Listing 4. Trap in function in sourced script
foo_deinit() is unable to unset the ERR trap, because errtrace is not set.

Trap in function in sourced script with ‘errtrace’

The solution is to set -o errtrace. See listing 5 underneath:

#!/bin/bash
#--- listing5.inc.sh ---------------------------------------------------
# Demonstration of ERR trap being reset by foo_deinit() with the use
# of `errtrace'.
# Example run:
#
#    $> set +o errtrace         # Make sure errtrace is not set (bash default)
#    $> trap - ERR              # Make sure no ERR trap is set (bash default)
#    $> . listing5.inc.sh       # Source listing5.inc.sh
#    $> foo                     # Run foo()
#    foo_init
#    foo
#    foo_deinit                 # This should reset the ERR trap...
#    $> trap                    # and it is indeed.
#    $> set +o | grep errtrace  # And the `errtrace' setting is restored.
#    $>
#
# See: http://fvue.nl/wiki/Bash:_Error_handling

function foo_init   { echo foo_init 
                      fooOldErrtrace=$(set +o | grep errtrace)
                      set -o errtrace
                      trap 'echo trapped' ERR   # Set ERR trap 
                    }
function foo_deinit { echo foo_deinit
                      trap - ERR                # Reset ERR trap
                      eval($fooOldErrtrace)     # Restore `errtrace' setting
                      fooOldErrtrace=           # Delete global variable
                    }
function foo        { foo_init
                      echo foo
                      foo_deinit             ;}
`Break’ trap in function in sourced script with `errtrace’

Everything combined in listing 6 underneath:

#!/bin/bash
#--- listing6.inc.sh ---------------------------------------------------
# Demonstration of ERR trap being reset by foo_deinit() with the use
# of `errtrace'.
# Example run:
#
#    $> set +o errtrace         # Make sure errtrace is not set (bash default)
#    $> trap - ERR              # Make sure no ERR trap is set (bash default)
#    $> . listing6.inc.sh       # Source listing6.inc.sh
#    $> foo                     # Run foo()
#    foo_init
#    Entered `trap-loop'
#    trapped
#    This is always executed - with or without a trap occurring
#    foo_deinit
#    $> trap                    # Check if ERR trap is reset.
#    $> set -o | grep errtrace  # Check if the `errtrace' setting is...
#    errtrace        off        # ...restored.
#    $>
#
# See: http://fvue.nl/wiki/Bash:_Error_handling

function foo_init {
    echo foo_init 
    fooOldErrtrace=$(set +o | grep errtrace)
    set -o errtrace
    trap 'echo trapped; break' ERR   # Set ERR trap 
}
function foo_deinit {
    echo foo_deinit
    trap - ERR                # Reset ERR trap
    eval $fooOldErrtrace      # Restore `errtrace' setting
    unset fooOldErrtrace      # Delete global variable
}
function foo {
    foo_init
        # `trap-loop'
    while true; do
        echo Entered `trap-loop'
        false
        echo This should never be reached because the `false' above is trapped
        break
    done
    echo This is always executed - with or without a trap occurring
    foo_deinit
}

Test

#!/bin/bash

    # Tests

    # An erroneous command should have exit status 127.
    # The erroneous command should be trapped by the ERR trap.
#erroneous_command

    #  A simple command exiting with a non-zero status should have exit status
    #+ <> 0, in this case 1.  The simple command is trapped by the ERR trap.
#false

    # Manually calling 'onexit'
#onexit

    # Manually calling 'onexit' with exit status
#onexit 5

    #  Killing a process via CTRL-C (signal 2/SIGINT) is handled via the SIGINT trap
    #  NOTE: `sleep' cannot be killed via `kill' plus 1/SIGHUP, 2/SIGINT, 3/SIGQUIT
    #+       or 15/SIGTERM.
#echo $$; sleep 20

    #  Killing a process via 1/SIGHUP, 2/SIGQUIT, 3/SIGQUIT or 15/SIGTERM is
    #+ handled via the respective trap.
    #  NOTE: Unfortunately, I haven't found a way to retrieve the signal number from
    #+       within the trap function.
echo $$; while true; do :; done

    # A syntax error is not trapped, but should have exit status 2
#fi

    # An unbound variable is not trapped, but should have exit status 1
    # thanks to 'set -u'
#echo $foo

     # Executing `false' within a function should exit with 1 because of `set -E'
#function foo() {
#    false
#    true
#} # foo()
#foo

echo End of script
   # Allways call 'onexit' at end of script
onexit

See also

Bash: Err trap not reset
Solution for trap - ERR not resetting ERR trap.

Journal

20210114

Another caveat: exit (or an error-trap) executed within «process substitution» doesn’t end outer process. The script underneath keeps outputting «loop1»:

#!/bin/bash
# This script outputs "loop1" forever, while I hoped it would exit all while-loops
set -o pipefail
set -Eeu
 
while true; do
    echo loop1
    while read FOO; do
        echo loop2
        echo FOO: $FOO
    done < <( exit 1 )
done

The ‘< <()’ notation is called process substitution.

See also:

  • https://mywiki.wooledge.org/ProcessSubstitution
  • https://unix.stackexchange.com/questions/128560/how-do-i-capture-the-exit-code-handle-errors-correctly-when-using-process-subs
  • https://superuser.com/questions/696855/why-doesnt-a-bash-while-loop-exit-when-piping-to-terminated-subcommand

Workaround: Use «Here Strings» ([n]<<<word):

#!/bin/bash
# This script will exit correctly if building up $rows results in an error
 
set -Eeu
 
rows=$(exit 1)
while true; do
    echo loop1
    while read FOO; do
        echo loop2
        echo FOO: $FOO
    done <<< "$rows"
done

20060524

#!/bin/bash
#--- traptest.sh --------------------------------------------
# Example script for trapping bash errors.
# NOTE: Why doesn't this scripts catch syntax errors?

    # Exit on all errors
set -e
    # Trap exit
trap trap_exit_handler EXIT


    # Handle exit trap
function trap_exit_handler() {
        # Backup exit status if you're interested...
    local exit_status=$?
        # Change value of $?
    true
    echo $?
    #echo trap_handler $exit_status
} # trap_exit_handler()


    # An erroneous command will trigger a bash error and, because
    # of 'set -e', will 'exit 127' thus falling into the exit trap.
#erroneous_command
    # The same goes for a command with a false return status
#false

    # A manual exit will also fall into the exit trap
#exit 5

    # A syntax error isn't catched?
fi

    # Disable exit trap
trap - EXIT
exit 0

Normally, a syntax error exits with status 2, but when both ‘set -e’ and ‘trap EXIT’ are defined, my script exits with status 0. How can I have both ‘errexit’ and ‘trap EXIT’ enabled, *and* catch syntax errors
via exit status? Here’s an example script (test.sh):

set -e
trap 'echo trapped: $?' EXIT
fi

$> bash test.sh; echo $?: $?
test.sh: line 3: syntax error near unexpected token `fi'
trapped: 0
$?: 0

More trivia:

  • With the line ‘#set -e’ commented, bash traps 258 and returns an exit status of 2:
trapped: 258
$?: 2
  • With the line ‘#trap ‘echo trapped $?’ EXIT’ commented, bash returns an exit status of 2:
$?: 2
  • With a bogus function definition on top, bash returns an exit status of 2, but no exit trap is executed:
function foo() { foo=bar }
set -e
trap 'echo trapped: $?' EXIT
fi
fred@linux:~>bash test.sh; echo $?: $?
test.sh: line 4: syntax error near unexpected token `fi'
test.sh: line 4: `fi'
$?: 2

20060525

Example of a ‘cleanup’ script

trap

Writing Robust Bash Shell Scripts

#!/bin/bash
#--- cleanup.sh ---------------------------------------------------------------
# Example script for trapping bash errors.
# NOTE: Use 'cleanexit [status]' instead of 'exit [status]'

    # Trap not-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM
    # @see catch_sig()
trap catch_sig 1 2 3 15
    # Trap errors (simple commands exiting with a non-zero status)
    # @see catch_err()
trap catch_err ERR


#--- cleanexit() --------------------------------------------------------------
#  Wrapper around 'exit' to cleanup on exit.
#  @param $1 integer  Exit status.  If $1 not defined, exit status of global
#+                    variable 'EXIT_STATUS' is used.  If neither $1 or
#+                    'EXIT_STATUS' defined, exit with status 0 (success).
function cleanexit() {
    echo "Exiting with ${1:-${EXIT_STATUS:-0}}"
    exit ${1:-${EXIT_STATUS:-0}}
} # cleanexit()


#--- catch_err() --------------------------------------------------------------
#  Catch ERR trap.
#  This traps simple commands exiting with a non-zero status.
#  See also: info bash | "Shell Builtin Commands" | "The Set Builtin" | "-e"
function catch_err() {
    local exit_status=$?
    echo "Inside catch_err"
    cleanexit $exit_status
} # catch_err()


#--- catch_sig() --------------------------------------------------------------
# Catch signal trap.
# Trap not-normal exit signals: 1/HUP, 2/INT, 3/QUIT, 15/TERM
# @NOTE1: Non-trapped signals are 0/EXIT, 9/KILL.
function catch_sig() {
    local exit_status=$?
    echo "Inside catch_sig"
    cleanexit $exit_status
} # catch_sig()


    # An erroneous command should have exit status 127.
    # The erroneous command should be trapped by the ERR trap.
#erroneous_command

    # A command returning false should have exit status <> 0
    # The false returning command should be trapped by the ERR trap.
#false

    # Manually calling 'cleanexit'
#cleanexit

    # Manually calling 'cleanexit' with exit status
#cleanexit 5

    # Killing a process via CTRL-C is handled via the SIGINT trap
#sleep 20

    # A syntax error is not trapped, but should have exit status 2
#fi

    # Allways call 'cleanexit' at end of script
cleanexit

blog comments powered by

Advertisement

blog comments powered by

bash не отменяет выполняемое выполнение в случае, если sth обнаруживает состояние ошибки (если вы не установили флаг -e). Языки программирования, предлагающие try/catch, делают это, чтобы препятствовать «спасению» из-за этой особой ситуации (отсюда обычно называют «исключение» ).

В bash вместо этого только команда, о которой идет речь, выйдет с кодом выхода больше 0, что указывает на состояние ошибки. Конечно, вы можете это проверить, но поскольку автоматическое спасение ни от чего не происходит, попытка/улов не имеет смысла. Это просто отсутствует в этом контексте.

Вы можете, однако, имитировать спасение, используя вспомогательные оболочки, которые могут заканчиваться в той точке, которую вы решите:

(
  echo "Do one thing"
  echo "Do another thing"
  if some_condition
  then
    exit 3  # <-- this is our simulated bailing out
  fi
  echo "Do yet another thing"
  echo "And do a last thing"
)   # <-- here we arrive after the simulated bailing out, and $? will be 3 (exit code)
if [ $? = 3 ]
then
  echo "Bail out detected"
fi

Вместо этого some_condition с if вы также можете просто попробовать команду, и в случае ее отказа (имеет код выхода больше 0) выйдите из системы:

(
  echo "Do one thing"
  echo "Do another thing"
  some_command || exit 3
  echo "Do yet another thing"
  echo "And do a last thing"
)
...

К сожалению, используя этот метод, вы ограничены 255 разными кодами выхода (1..255), и никакие объекты исключающего исключения не могут быть использованы.

Если вам нужна дополнительная информация для передачи вместе с вашим симулированным исключением, вы можете использовать stdout подоболочек, но это немного сложнее и, возможно, другой вопрос: -)

Используя вышеупомянутый флаг -e для оболочки, вы можете даже удалить этот явный оператор exit:

(
  set -e
  echo "Do one thing"
  echo "Do another thing"
  some_command
  echo "Do yet another thing"
  echo "And do a last thing"
)
...

Понравилась статья? Поделить с друзьями:
  • Catch error jenkins
  • Catch error in vba
  • Catch error groovy
  • Catch error flutter
  • Catch error 404