1

I am working on to "reuse" the error handling logic for my bash script. I think the "set -e" and "trap" command not getting alone well and I cannot figure out why. It would be really great if anyone can share a link or doc to explain this behaviour, thank you.

What do I expect

Error on line 5

What have I got

nothing...

[test.sh]

#!/bin/bash
source util-shared_script.sh

main(){
    echo hello | grep foo  # This generates error
}

main

[util-shared_script.sh]

#!/bin/bash
set -e
failure() {
  local lineno=$1
  local msg=$2
  echo "Failed at line: $lineno; command: $msg"
}
trap 'failure ${LINENO} "$BASH_COMMAND"' ERR
GreenLake4964
  • 744
  • 9
  • 20
  • 2
    When I run your script(s) in bash 5.0.3 I get `Failed at line: 9; command: grep foo`. What's your bash version? Have you tried to reproduce the problem yourself from scratch (for instance, traps from your interactive shell also affect the script)? – Socowi Jun 15 '20 at 11:46
  • thanks, I missed a "set -e". What I intended to do is to stop the script whenever there is an error, and capture line number. – GreenLake4964 Jun 15 '20 at 12:04
  • 3
    Why use `set -e` at all when you have an ERR trap and can just add an `exit` to it (after capturing `$?` as the first line and using that variable in the last one) to do the same thing? – Charles Duffy Jun 15 '20 at 12:46
  • 2
    Mind you, *both* are unreliable and not best practice to use; see [BashFAQ #105](https://mywiki.wooledge.org/BashFAQ/105), including the exercises therein. Both `set -e` and ERR traps trigger only on "unchecked" exceptions, and what makes a command's exit status be considered checked is a complicated, unintuitive, and highly variable (between shells and versions of the same shell) question. – Charles Duffy Jun 15 '20 at 12:47
  • 2
    ...consequently, it's hard to predict what code using `set -e` or ERR traps will do at runtime during a code read; a function that behaves one way when run on its own will behave a different way when called from another function used in the condition section of an `if`, for example. This makes it hard to accurately review, and (as you're here discovering) hard to write with an accurate mental model of runtime behavior. More robust practice is to do explicit error handling. – Charles Duffy Jun 15 '20 at 12:54

1 Answers1

1

@Charles Thanks for the suggestions. I will seriously consider about the pros and cons to improve my code.

For now, I found a stackoverflow link that is related to my issue. After I replaced "set -e" with "set -eE -o functrace", I get what I expect to achieve.

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 

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.

GreenLake4964
  • 744
  • 9
  • 20