Flag for Exiting on non-zero (set -e)

TLDR: set -e is NOT full proof and comes with caveats.

Arguments against set -e

Example

✅ Basic example works well
set -e

foo() {
  echo "This is foo; returns 1 (unhappy)"
  return 1
}

main() {
  foo

  echo.green "MAIN FINISHED! (after foo calls)"
}

main "${@}"
m:mac d:kotlin-mp b:master ○❯sr
This is foo; returns 1 (unhappy)
m:mac d:kotlin-mp b:master ○❯
❌ Checking for failure/success, even up the chain prevents 'set -e' from triggering.❌

To show in an example, how set -e is prevented from triggering if up the chain we have a success or error branch.

Let's say we have the following code.

#!/usr/bin/env bash

set -e

echo_with_pid(){
  echo "[\$\$=$$/$BASHPID] $*"
}

foo2() {
  echo_with_pid "foo2-enter"
  i_dont_exist
  echo_with_pid "foo2-exit"
}

foo1() {
  echo_with_pid "foo1-enter"
  foo2
  echo_with_pid "foo1-exit"
}

main() {
  echo_with_pid "START"
  foo1
  echo_with_pid "DONE"
}
# If we just call main, `set -e` works as one would expect
main

Aborts execution and giving us the following output:

[$$=37290/37290] START
[$$=37290/37290] foo1-enter
[$$=37290/37290] foo2-enter
/tmp/scratch.sh: line 11: i_dont_exist: command not found

BUT, if we were to check the return code of main, the behavior becomes quite un-intuitive.

# If we run with having just a success branch (no failure branch), the not-found command get's masked.
if main; then
  echo "Main succeeded"
fi
❯/tmp/scratch.sh
[$$=38049/38049] START
[$$=38049/38049] foo1-enter
[$$=38049/38049] foo2-enter
/tmp/scratch.sh: line 11: i_dont_exist: command not found
[$$=38049/38049] foo2-exit
[$$=38049/38049] foo1-exit
[$$=38049/38049] DONE
Main succeeded

command_not_found_handle does not have this issue and is able to catch

⚠️ 'local' used on the same line will prevent 'set -e' from aborting

set -e

foo() {
  echo "This is foo; returns 1 (unhappy)"
  return 1
}

main() {
  echo "Calling foo()"
  # We succeed because of 'local' being used on the same line as the function call.
  local foo_capture="$(foo)"
  echo "After foo()"

  echo.green "MAIN FINISHED! (after foo calls), foo_capture=[${foo_capture}]"
}

main "${@}"

Output:

m:mac d:kotlin-mp b:master ○❯sr
Calling foo()
After foo()
MAIN FINISHED! (after foo calls), foo_capture=[This is foo; returns 1 (unhappy)]
m:mac d:kotlin-mp b:master ○❯

IF we were to split up declaration of foo_capture into

local foo_capture
foo_capture="$(foo)"

Then set -e would trigger and script would stop.

⚠️ Some commands return non-zero in non-error cases - By default with 0 visibility
#!/usr/bin/env bash

set -e
echo_with_pid(){
  echo "[\$\$=$$/$BASHPID] $*"
}

main() {
  echo_with_pid "START"

  echo "1" > /tmp/1
  echo "2" > /tmp/2

  # Diff will return 1 if files are not the same, this will abort the script.
  # 
  # In this case we would have to adjust the code to be 
  #   diff /tmp/1 /tmp/2 || true
  # 
  # For the script to NOT abort.
  diff /tmp/1 /tmp/2

  echo ""
  echo_with_pid "DONE"
}

main "${@}"

Output

[$$=26314/26314] START
1c1
< 1
---
> 2

The more annoying part is not having to write

diff /tmp/1 /tmp/2 || true

But that when set -e halts it does NOT provide any debugging info on WHY it stopped.

For Searchability

For Searchability

set -o pipefail


Children
  1. ⚠️ 'local' used on the same line with function call will prevent 'set -e' from aborting script
  2. ❌ Checking for failure/success, even up the chain prevents 'set -e' from triggering.❌

Backlinks