2

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.

2
  • 2
    Option 3 is actually a good idea. Commented Dec 6, 2016 at 19:18
  • @chepner: Good read. cmd || return 1 is explicit and conceptually ok, but so cumbersome... Commented Dec 6, 2016 at 19:44

2 Answers 2

2

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).

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks! I had tested with trap ... ERR, but set -E was missing to make it work. I think it's a pretty cool workaround, you don't need to change the code itself.
I have problems when f is called from an if: if ! f; then echo "f failed"; fi -> [f] Fail! I don't want this executed
@tokland: I'm afraid that's by design - please see my update.
Interesting use of set -E and well explained answer ++
1

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 " 

4 Comments

Very interesting, I didn't know about this approach. A bit cumbersome to write, though. I'll add some echos to f to make declare -p out show something meaningful.
Yes that's correct output because after that false command makes function f exit.
Interesting alternative - set -e indeed applies to the commands inside a process substitution as well (as opposed to a command substitution), though a failure there won't abort the script as a whole. Note that the commands in a process substitution run in a subshell too, which you can verify as follows: f() { echo $((++v)); }; v=1; cat <(f); echo "$v"
Yes you're right. So set -e is in effect for subshell from process substitution but not from command substitution

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.