Reputation: 67900
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
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
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