Bash error handling
Материал из noname.com.ua
Версия от 14:51, 27 октября 2023; Sirmax (обсуждение | вклад)
http://fvue.nl/wiki/Bash:_Error_handling
Bash: Error handling From FVue Jump to: navigation, search 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? To -e or not to to -e? Opinions differ about whether it's wise to use set -e, because of its seemingly non-intuitive problems... In favour of -e: Use set -e - Writing Robust Bash Shell Scripts - David Pashley Doubtful about -e: Why doesn't set -e (or set -o errexit, or trap ERR) do what I expected? - BashFAQ/105 - Greg's Wiki 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: Specify `bash -e' as shebang interpreter Start shell script with `bash -e' Use `set -e' in shell script Use `set -o errexit' in shell script 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