On Thu, Sep 24, 2026 at 08:10:30PM PDT, Martin D Kealey wrote:
The significant change isn't any combination of «set -e», «set -o errexit»
and/or «shopt -s execfail», but rather the insertion of «exec».
The "exit on failure" is one of the minor differences between an
interactive shell and a non-interactive one, including a subshell of an
interactive shell
It is sufficiently demonstrated by:
( exec /no/such/path || echo In Subshell )
exec /no/such/path || In Interactive Shell
without any use of set-e, errexit, or execfail.
-Martin
Sorry, I don't see how interactive shells enter the picture here; all of
the examples I provided were of non-interactive shells.
The documentation of the 'execfail' option says:
If set, a non-interactive shell will not exit if it
cannot execute the file specified as an argument to the
exec builtin.
So when I enable it, I'd expect that to mean that the shell would
continue running after a failed exec. And without 'set -e' (a.k.a.
errexit), it does:
$ bash -c 'shopt -s execfail; exec /bin/enoent; echo foo'
bash: line 1: /bin/enoent: No such file or directory
foo
And yes, with 'set -e' enabled, it is expected that the shell exits
immediately when a command fails, so this is expected behavior as well:
$ bash -e -c 'shopt -s execfail; exec /bin/enoent; echo not printed'
bash: line 1: /bin/enoent: No such file or directory
However, as an exception to that immediate-exit behavior, the
documentation for errexit says:
The shell does not exit if the command that fails is
[...] part of the test following the if or elif reserved words [...]
Given that, I would expect the following to print "failed":
$ bash -e -c 'shopt -s execfail; if ! exec /bin/enoent; then echo failed; fi'
bash: line 1: /bin/enoent: No such file or directory
But instead it exits immediately after the failed exec, specifically via
the exit_immediately_on_error path in report_error() in error.c.
I'm not certain if this entirely an appropriate fix, but it seems to
pass 'make test':
diff --git execute_cmd.c execute_cmd.c
index 6a22395e0cde..42bb4f71dd2a 100644
--- execute_cmd.c
+++ execute_cmd.c
@@ -4997,8 +4997,9 @@ execute_builtin (sh_builtin_func_t *builtin, WORD_LIST
*words, int flags, int su
value of the command, we turn the -e flag off ourselves and disable
the ERR trap, then restore them when the command completes. This is
also a problem (as below) for the command and source/. builtins. */
+ extern int no_exit_on_failed_exec;
if (subshell == 0 && (flags & CMD_IGNORE_RETURN) &&
- (builtin == eval_builtin || (flags & CMD_COMMAND_BUILTIN) || builtin ==
source_builtin))
+ (builtin == eval_builtin || (flags & CMD_COMMAND_BUILTIN) || builtin ==
source_builtin || (builtin == exec_builtin && no_exit_on_failed_exec)))
{
begin_unwind_frame ("eval_builtin");
unwind_protect_int (exit_immediately_on_error);
With that patch applied, I see the behavior I'd expect:
$ ./bash -e -c 'shopt -s execfail; exec /bin/enoent || echo foo'
./bash: line 1: /bin/enoent: No such file or directory
foo
It also lets a defined EXIT trap run:
$ ./bash -e -c 'trap "echo trapped" EXIT; shopt -s execfail; exec /bin/enoent
|| :'
./bash: line 1: /bin/enoent: No such file or directory
trapped
Whereas with unmodified bash 5.3.0 the trap doesn't run:
$ bash -e -c 'trap "echo trapped" EXIT; shopt -s execfail; exec /bin/enoent ||
:'
bash: line 1: /bin/enoent: No such file or directory
Thanks,
Zev