tokland
tokland

Reputation: 67900

Idiomatic way of forcing set -e everywhere in a shell script

Let's imagine we have this code (script.sh):

#!/bin/bash
set -e

f() {
  echo "[f] Start"  >&2
  echo "f:before-false1"
  echo "f:before-false2"
  false
  echo "f:after-false"
  echo "[f] Fail! I don't want this executed" >&2
}

out=$(f)

The output:

$ bash myscript.sh
[f] Start
[f] Fail! I don't want this executed

I understand that $(...) starts a sub-shell where set -e is not propagated, so my question is: what's the idiomatic way to make this run as expected without too much clutter? I can see 3 solutions, none of which I like (nor I am actually sure they indeed work): 1) Add set -e to the start of f (and every other function in the app). 2) Run $(set -e && f). 3) Add ... || return 1 to every command that may fail.

Upvotes: 2

Views: 398

Answers (2)

mklement0
mklement0

Reputation: 440422

It's not the prettiest solution, but it does allow you to emulate set -e for the current shell as well as any functions and subshells:

#!/bin/bash

# Set up an ERR trap that unconditionally exits with a nonzero exit code.
# Similar to -e, this trap is invoked when a command reports a nonzero
# exit code (outside of a conditional / test).
# Note: This trap may be called *multiple* times. 
trap 'exit 1' ERR

# Now ensure that the ERR trap is called not only by the current shell,
# but by any subshells too:
# set -E (set -o errtrace) causes functions and subshells to inherit
# ERR traps.
set -E    

f() {
  echo "[f] Start"  >&2
  echo "f:before-false1"
  echo "f:before-false2"
  false
  echo "f:after-false"
  echo "[f] Fail! I don't want this executed" >&2
}

out=$(f)

Output (to stderr) if you call this script (exit code afterward will be 1) - note how the 2nd echo to stderr (>&2) is not printed, proving that the execution of false aborted the command substitution:

[f] Start

Note:

  • By design, set -e / trap ERR only respond to failures that aren't part of conditionals (see man bash, under the description of set (search for literal " set ["), for the exact rules, which changed slightly between Bash 3.x and 4.x).

    • Thus, for instance, f does NOT trigger the trap in the following commands: if ! f; then ..., f && echo ok; the following triggers the trap in the subshell (command substitution $(...), but not in the enclosing conditional ([[ ... ]]): [[ $(f) == 'foo' ]] && echo ok, so the script as a whole doesn't abort.

    • To exit a function / subshell explicitly in such cases, use something like || return 1 / || exit 1, or call the function / subshell separately, outside of a conditional first; e.g., in the [[ $(f) == 'foo' ]] case: res=$(f); [[ $res == 'foo' ]] - res=$(f) will then trigger the trap for the current shell too.

  • As for why the trap code may be invoked multiple times: In the case at hand, false inside f() first triggers the trap, and then, because the trap's exit 1 exits the subshell ($(f)), the trap is triggered again for the current shell (the one running the script).

Upvotes: 2

anubhava
anubhava

Reputation: 786091

Instead of command substitution, you should use process substitution to call your function so that set -e remains in effect:

mapfile arr < <(f)   # call function f using process substitution
out="${arr[*]}"         # convert array content into a string
declare -p out          # check output

Output:

[f] Start
declare -- out="f:before-false1
 f:before-false2
"

Upvotes: 1

Related Questions