21

In the bash man page, it states:

Exit immediately if a pipeline (which may consist of a single simple command),
a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces...

So I assumed that a function should be considered a command list enclosed by braces. However, if you apply a conditional to the function call, errexit no longer persists inside the function body and it executes the entire command list before returning. Even if you explicitly create a subshell inside the function with errexit enabled for that subshell, all commands in the command list are executed. Here is a simple example that demonstrates the issue:

function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }

( set -Eex ; a )
+ a
+ b
+ false

( set -Eex ; ap )
+ ap
+ b
+ false

( set -Eex ; as )
+ as
+ set -e
+ b
+ false

Now if I apply a conditional to each of them...

( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false

( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false

( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
Craig
  • 4,268
  • 4
  • 36
  • 53
  • 1
    Similar: [How do I get effect and usefuless of “set -e” inside a shell function?](https://stackoverflow.com/q/4072984/55075) – kenorb Jan 03 '19 at 15:06

4 Answers4

18

You started to quote the manual but then you cut the bit that explained this behaviour, which was in the very next sentence:

-e Exit immediately if a pipeline, which may consist of a single simple command, a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces returns a non-zero status. 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 !.

Gareth Rees
  • 64,967
  • 9
  • 133
  • 163
  • 2
    Okay, so is there a way to explicitly make something fail in a subshell without sticking `||` on the end of each command in the command list? – Craig Nov 05 '13 at 13:26
  • 2
    In your cite is nothing about functions. – Olleg Sep 17 '19 at 15:45
  • 1
    @Olleg, that was confusing to me too. I think it's because the function is being called as part of a statement with a `||` so it disables the `set -e` – ZombieDev Jun 15 '20 at 14:25
  • 2
    it really makes functions that have commands that might fail completely worthless. You'd have to instead do a bunch of `$?` checking in the function to make sure and exit at the correct spots. otherwise it won't work inside other pipelines or if conditionals, or whatever needs it to evaluate to false when it fails. pretty annoying if you ask me. – ZombieDev Jun 15 '20 at 14:29
  • 1
    FYI, this very frustrating behaviour of ignoring the shell option also applies to `set -E` and ERR traps. – jmrah Jul 02 '20 at 11:53
  • 1
    This (maddening IMHO) behaviour also applies to sourced scripts: `set -e; source script.sh || true` leads Bash to ignore `set -e` for the entirety of the sourced script. – ack Jul 30 '20 at 22:08
  • The note that @ZombieDev adds above, that this applies even to functions because their content is considered part of the `||`-statement (more exactly, `set -e` is turned off before the `||` and this applies also to function bodies, which is what "imz" says in their answer), is IMO the most important note, but it was hidden to me because it had only 0 upvotes. Should this not be added to the answer? – Johannes Jul 08 '21 at 05:12
  • this answer explain **why** the undesirable behavior happens --> for a solution [go here to see how you can still call a bash function and use `set -o errexit`](https://stackoverflow.com/a/11092989/52074) – Trevor Boyd Smith Feb 06 '23 at 19:24
13

bug-bash mailing list has an explanation by Eric Blake more explicit about functions:

Short answer: historical compatibility.

...

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.

And some words about whether set -e can be exploited to achieve the wanted behavior:

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.

imz -- Ivan Zakharyaschev
  • 4,921
  • 6
  • 53
  • 104
  • 6
    what a disappointed answer. maybe `bash` should provide a "non-standard" option to override this standard but surprising, wrong behavior. – Penghe Geng Aug 05 '18 at 14:45
  • 1
    _But it is standardized, so we have to live with it_. I would take a guess and say this is mandated by the POSIX standard. Cannot be easily changed, otherwise why follow standards at all? – Felipe Alvarez Dec 03 '19 at 01:29
  • 3
    You don't have to live with it. Just don't follow the standard. – Steven Shaw Jul 30 '20 at 09:34
2

Not an answer to the original question, but a work-around for the underlying problem: set up a trap on errors:

function on_error() {
    echo "error happened!"
}
trap on_error ERR

echo "OK so far"
false
echo "this line should not execute"

The reason for the behavior itself is properly explained in other answers (basically it's expected bash behavior as per the manual and POSIX): https://stackoverflow.com/a/19789651/1091436

VasiliNovikov
  • 9,681
  • 4
  • 44
  • 62
  • 1
    In my tests, once a command is part of a conditional statement, ERR traps are completely stripped from all functions in the callstack from that call forward (just like the OP's `-e`), so I'm not sure what you mean exactly. Could you clarify what you mean? – jmrah Jul 02 '20 at 11:51
  • @jrahhali I'm not sure what your question is, but if I execute the script above, I get the expected order of things: writing "OK so far", then writing "error happened!", then exiting. – VasiliNovikov Jul 02 '20 at 12:04
  • Odd. The "this line should not execute" prints for me when I run it as a script. What is the output of "$-" for you when you run the script? – jmrah Jul 02 '20 at 12:14
  • @jrahhali you should also set bash to exit on errors at all of course. Otherwise `false` won't do anything in the script. `set -e` in the beginning of the file – VasiliNovikov Jul 02 '20 at 13:09
  • yes, using `set -e` will make *your script work. But I'm not sure how ERR traps are a work-around for the OP's problem. – jmrah Jul 02 '20 at 14:54
-1

not an answer but you might fix this counter intuitive behaviur by defining this helper function:

fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }

then call your functions via fixerrexit.

example:

f1()
{
  mv unimportant-0.txt important.txt
  rm unimportant-*.txt
}

set -e

if fixerrexit f1
then
  echo here is the important file: important.txt
  echo unimportant files are deleted
fi

if outer context has errexit on, then fixerrexit turns on errexit inside f1() as well, so you dont need to worry about commands being executed after a failure occurs.

the only downside is you can not set variables since it runs f1 inside a subshell.

bandie
  • 173
  • 1
  • 11