Bash: Error handling

From FVue
Jump to: navigation, search

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

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 Disqus

Comments

blog comments powered by Disqus