80

I have a simple script :

#!/bin/bash
set -e
trap "echo BOO!" ERR 

function func(){
    ls /root/
}

func

I would like to trap ERR if my script fails (as it will here b/c I do not have the permissions to look into /root). However, when using set -e it is not trapped. Without set -e ERR is trapped.

According to the bash man page, for set -e :

... A trap on ERR, if set, is executed before the shell exits. ...

Why isn't my trap executed? From the man page it seems like it should.

codeforester
  • 39,467
  • 16
  • 112
  • 140
irritable_phd_syndrome
  • 4,631
  • 3
  • 32
  • 60
  • 5
    As an aside: it's better to _single-quote_ your trap handlers, unless you explicitly want variable references in it expanded _up front_. – mklement0 Mar 04 '16 at 17:22

4 Answers4

94

chepner's answer is the best solution: If you want to combine set -e (same as: set -o errexit) with an ERR trap, also use set -o errtrace (same as: set -E).

In short: use set -eE in lieu of just set -e:

#!/bin/bash

set -eE  # same as: `set -o errexit -o errtrace`
trap 'echo BOO!' ERR 

function func(){
  ls /root/
}

# Thanks to -E / -o errtrace, this still triggers the trap, 
# even though the failure occurs *inside the function*.
func 

A more sophisticated example trap example that prints the message in red and also prints the exit code:
trap 'printf "\e[31m%s: %s\e[m\n" "BOO!" $?' ERR


man bash says about set -o errtrace / 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.

What I believe is happening:

  • Without -e: The ls command fails inside your function, and, due to being the last command in the function, the function reports ls's nonzero exit code to the caller, your top-level script scope. In that scope, the ERR trap is in effect, and it is invoked (but note that execution will continue, unless you explicitly call exit from the trap).

  • With -e (but without -E): The ls command fails inside your function, and because set -e is in effect, Bash instantly exits, directly from the function scope - and since there is no ERR trap in effect there (because it wasn't inherited from the parent scope), your trap is not called.

While the man page is not incorrect, I agree that this behavior is not exactly obvious - you have to infer it.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks, I will. Also instead of `ls /root/` when I run `bash try.sh` which has `#!/bin/bash exit 1`, it does not catched by the `trap`, is there any way to catch the called script if it is exited ? – alper Jul 02 '20 at 18:38
  • 1
    @alper There's a separate `EXIT` trap invoked when the script exits, for whatever reason, and whether successfully or not. To only act on _non-zero_ exit codes, you must check `$?`; e.g.: `trap 'ec=$?; (( ec != 0 )) && echo "Exited with failure: $ec"' EXIT` – mklement0 Jul 03 '20 at 13:18
17

You need to use set -o errtrace for the function to inherit the trap.

chepner
  • 497,756
  • 71
  • 530
  • 681
13

We have these options for debugging:

  • -e Exit immediately on failure
  • -E If set, any trap on ERR is inherited by shell functions
  • -u Exit when there is an unbound variable
  • -o Give a option-name to set
    • pipefail The return values of last (rightmost) command (exit code)
  • -v Print all shell input lines as they are read
  • -x Print trace of commands

For handling the errors we can catch directory with trap

trap 'echo >&2 "Error - exited with status $? at line $LINENO' ERR

Or a better version ref :

trap 'echo >&2 "Error - exited with status $? at line $LINENO:";
         pr -tn $0 | tail -n+$((LINENO - 3)) | head -n7' ERR

Or a function:

function __error_handing__(){
    local last_status_code=$1;
    local error_line_number=$2;
    echo 1>&2 "Error - exited with status $last_status_code at line $error_line_number";
    perl -slne 'if($.+5 >= $ln && $.-4 <= $ln){ $_="$. $_"; s/$ln/">" x length($ln)/eg; s/^\D+.*?$/\e[1;31m$&\e[0m/g;  print}' -- -ln=$error_line_number $0
}

and call it this way:

trap  '__error_handing__ $? $LINENO' ERR
Shakiba Moshiri
  • 21,040
  • 2
  • 34
  • 44
5

Replace ERR with EXIT and it will work.

The syntax of the trap command is: trap [COMMANDS] [SIGNALS]

For more info, please read http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html

mklement0
  • 382,024
  • 64
  • 607
  • 775
Ritesh
  • 1,809
  • 1
  • 14
  • 16
  • 19
    While this is a viable workaround (albeit not needed, if `set -o errtrace` is used), you should mention that `EXIT` is also called in the event of _successful_ termination, so you'd need to add a conditional such as `if [[ $? -ne 0 ]] …` to the handler. – mklement0 Mar 04 '16 at 17:12