set -e inconsistency inside blocks

Created: 2019-02-15 — modified: 2019-02-18 — tags: Bash

set -e might work not as you expected inside (parentheses).

You know what set -e does, right? It aborts execution as soon as one of commands returns non-0 result. For example, the following code:

set -e
true 1
false 2
true 3

will be executed like this:

$ bash -x test.sh
+ set -e
+ true 1
+ false 2

Note that here I'm using the true debugging trick mentioned in previous blog post. Above is true for parenthesised blocks, too:

set -e
(
    true 1
    false 2
    true 3
)
true 4

Output of running this script will be exactly the same as before.

But what if you want to continue after failing block? For example, writing this:

set -e
(
    true 1
    false 2
    true 3
) || true 4
true 5

One might expect parenthesised block to be aborted after false 2 command, then "alternative" command true 4 to be executed, and execution continue. But actual execution is like this:

$ bash -x test.sh
+ set -e
+ true 1
+ false 2
+ true 3
+ true 5

Before you ask - it will be the same even if you add extra set -e inside the block, too. What happens is that, it seems, set -e is totally ignored inside the block, happily continuing to true 3 statement, and its return code is used as return code of the whole braced statement - hence true 4 command is not executed.

Same happens with parentheses inside if:

$ cat test.sh
set -e
if (
    true 1
    false 2
    true 3
); then
    true pass
else
    true fail
fi
$ bash -x test.sh
+ set -e
+ true 1
+ false 2
+ true 3
+ true pass

 

Workarounds

First workaround is putting && at the end of each line, like this:

$ cat test.sh
set -e
(
    true 1 &&
    false 2 &&
    true 3
) || true 4
true 5
$ bash -x test.sh
+ set -e
+ true 1
+ false 2
+ true 4
+ true 5

But well, in that case you can omit parentheses altogether:

set -e
    true 1 &&
    false 2 &&
    true 3 ||
    true 4
true 5

Not sure what indentation should look like, though.

Another workaround is making it set +e on the outside, set -e on the inside, and preserving return code, like this:

$ cat test.sh
set -e
set +e
(
    set -e
    true 1
    false 2
    true 3
)
result=$?
set -e
test "$result" = "0" || true 4
true 5
$ bash -x test.sh
+ set -e
+ set +e
+ set -e
+ true 1
+ false 2
+ result=1
+ set -e
+ test 1 = 0
+ true 4
+ true 5