7

AFAICT, having continue in for loop that calls another function breaks the errexit semantics. In the main() function, I want to continue onto the next iteration if anything fails in the build() function:

#! /usr/bin/env bash

export PS4='# ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]}() - [${SHLVL},${BASH_SUBSHELL},$?] '
set -o xtrace
set -o errexit

build() {
  local _foo=$1

  if [ "${_foo}" -eq 1 ]; then
    false
  fi

  printf "%s with foo=%s builds ok\\n" "${FUNCNAME[0]}" "${_foo}"
}

main() {
  for i in 1 2 3; do
    build $i || continue
  done
}

main "$@"

However, continue inside the for loop causes the code to continue inside the build() function instead, removing the effect of the errexit flag:

$ ./foo.sh 
# ./foo.sh:5: () - [3,0,0] set -o errexit
# ./foo.sh:23: () - [3,0,0] main
# ./foo.sh:18: main() - [3,0,0] for i in 1 2 3
# ./foo.sh:19: main() - [3,0,0] build 1
# ./foo.sh:8: build() - [3,0,0] local _foo=1
# ./foo.sh:10: build() - [3,0,0] '[' 1 -eq 1 ']'
# ./foo.sh:11: build() - [3,0,0] false
# ./foo.sh:14: build() - [3,0,1] printf '%s with foo=%s builds ok\n' build 1
build with foo=1 builds ok
# ./foo.sh:18: main() - [3,0,0] for i in 1 2 3
# ./foo.sh:19: main() - [3,0,0] build 2
# ./foo.sh:8: build() - [3,0,0] local _foo=2
# ./foo.sh:10: build() - [3,0,0] '[' 2 -eq 1 ']'
# ./foo.sh:14: build() - [3,0,0] printf '%s with foo=%s builds ok\n' build 2
build with foo=2 builds ok
# ./foo.sh:18: main() - [3,0,0] for i in 1 2 3
# ./foo.sh:19: main() - [3,0,0] build 3
# ./foo.sh:8: build() - [3,0,0] local _foo=3
# ./foo.sh:10: build() - [3,0,0] '[' 3 -eq 1 ']'
# ./foo.sh:14: build() - [3,0,0] printf '%s with foo=%s builds ok\n' build 3
build with foo=3 builds ok

As you can see on the line with the printf, the exit code of the previous line, the false, is indeed 1 (the third number inside the bracket in front of it), so it is running as if errexit wasn't in place:

# ./foo.sh:14: build() - [3,0,1] printf '%s with foo=%s builds ok\n' build 1

I've confirmed that removing the || continue makes the shell exit when i=1, so the errexit is passed onto the subhshell/function.

Any help would be much appreciated.

Versions

~ $ bash --version                                                            
GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)

Update

Lots of good answers as to why this is. As for how to solve it, I've found this solution to be the easiest to make the script to what I want: Changing the false to:

false || return $?

The drawback of course, is that I'll have to do that for all the commands the function calls out to. I might have to go back to my old approach of using a run() wrapper, which executes the passed command, checks the return code of it and fails the script accordingly. Doing what you would expect errexit to do, I suppose :-)

2
  • Well, if errexit worked the way you propose at the end, it would again be counterintuitive. It would have to be called errreturn or so. errexit means "on error, exit the shell", and that would have been completely useless if it exited the shell every time an if condition evaluated to false.
    – TooTea
    Commented Apr 16, 2021 at 18:48
  • The best way to avoid the whole issue is to stop using Perl-isms like || continue in BASH and instead do if [[ $? != 0 ]]; then continue; fi. That works perfectly fine with errexit without having to modify or wrap everything you call.
    – TooTea
    Commented Apr 16, 2021 at 18:51

2 Answers 2

7

This seems to match the description of -e/-errexit in the bash documentation:

The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.
[...]

If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status.

This has been covered in this stackoverflow question, which links to this email with the following text:

> My initial gripe about errexit (and its man page description) is that the 
> following doesn't behave as a newbie would expect it to:
> 
> set -e
> f() {
>   false
>   echo "NO!!"
> }
> f || { echo "f failed" >&2; exit 1; }

Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive.  But
it is standardized, so we have to live with it.

The POSIX description of -e says:

-e
When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value >0, and is not part of the compound list following a while, until, or if keyword, and is not a part of an AND or OR list, and is not a pipeline preceded by the ! reserved word, then the shell shall immediately exit.

4
  • 1
    Thanks for links and explanation, @Wieland. I can see I'm not the only one finding this highly illogical. Getting the historic context makes it easier to accept, like this quote from Eric Blake: > Because once you are in a context that ignores 'set -e', the historical behavior is that there is no further way to turn it back on, for that entire body of code in the ignored context. That's how it was done 30 years ago, before shell functions were really thought about, and we are stuck with that poor design decision.
    – skybert
    Commented Apr 16, 2021 at 8:50
  • Looking at that POSIX text quoted there, it seems a bit oddly worded to me. It says "not a part of an AND or OR list", but it could well be argued that the false there isn't part of one, it's just a simple command within a function. But a newer version phrases that a bit differently: "The -e setting shall be ignored when executing [...] any command of an AND-OR list other than the last." which I guess the shell would be doing while executing the function. Bash's phrasing is clearer again, though.
    – ilkkachu
    Commented Apr 16, 2021 at 10:17
  • Also this is BashFAQ 105
    – ilkkachu
    Commented Apr 16, 2021 at 10:19
  • 1
    An additional subtle implication that may be overlooked at a first glance at the Bash's phrasing is that the "ignore--e state" is effective, in a negation and AND/OR list, no matter what. This means that not even within subshells can one undo such "ignore-state" if the subshell is part (directly or not) of a conditional construct. So POSIX's own example set -e; (false; echo one) | cat; echo two displays two but set -e; ! (false; echo one) | cat; echo two (note the ! before the subshell) displays both one and two. Analogous result in AND-OR lists.
    – LL3
    Commented Apr 16, 2021 at 22:36
4

From the manual [emphasis mine]:

errexit

Same as -e.

-e

Exit immediately if a pipeline […], which may consist of a single simple command […], a list […], or a compound command […] returns a non-zero status. The shell does not exit if the command that fails is part of […] any command executed in a && or || list except the command following the final && or ||, […]

[…]

If a […] shell function executes in a context where -e is being ignored, none of the commands executed within the […] function body will be affected by the -e setting, even if -e is set and a command returns a failure status. […]

In your build $i || continue build is a shell function that executes in a context where -e is being ignored. false is a command executed within the function body, it is not affected by the -e setting and therefore it doesn't even prevent printf from running.

Removing the || continue and invoking just build $i places every part of the function in a context where -e is not being ignored, so the entire code exits because of false and just after false (without getting to printf).

It seems errexit is a global setting that (when not being ignored) terminates the entire script. One cannot (or at least cannot easily) make it terminate a function but not the entire script.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .