I'm seeing some weird behavior with bash and trapping EXIT
inside subshells. I'd expect the four following lines to all output the same thing ("hi trapped"):
a=$(trap 'echo trapped' EXIT ; echo hi); echo $a
a=$(trap 'echo trapped' EXIT && echo hi); echo $a
a=$(trap 'echo trapped' EXIT ; /bin/echo hi); echo $a
a=$(trap 'echo trapped' EXIT && /bin/echo hi); echo $a
The first three do print "hi trapped", but not the last one. It just outputs "hi". The trap is not being called. You can verify this with set -x
:
set -x; a=$(trap 'echo trapped' EXIT ; echo hi); set +x; echo $a
set -x; a=$(trap 'echo trapped' EXIT && echo hi); set +x; echo $a
set -x; a=$(trap 'echo trapped' EXIT ; /bin/echo hi); set +x; echo $a
set -x; a=$(trap 'echo trapped' EXIT && /bin/echo hi); set +x; echo $a
Through some trial and error I've found that the EXIT
trap is not called under the following conditions:
&&
.;
, or even ||
at any point, the trap will execute.Is this intentional? Is it documented?
For reference, I came across this because rvm overwrites cd
with its own function that ends up adding a trap on EXIT
which does (among other things) echo -n 'Saving session...'
. I was running a shell script that uses this bash idiom:
some_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )
So some_dir
was getting 'Saving session...' appended to it. It was hard to debug, because subshells weren't always running the EXIT
trap rvm was adding.
Addendum: As noted in comments below, I'm not seeing this weird behavior in bash-5.1.16, the latest version I did see the weird behavior in was bash-5.0.2.
I used strace -e clone,execve -f -p $$&
to see what the current shell is doing when running echo version and /bin/echo version. I put a &
so that it will continue to read commands.
In the /bin/echo version, I believe bash did an shortcut and execve-ed the () subshell for /bin/echo, so the trap is not there anymore (traps do not survive execve, I guess).
In the bare echo version, it's a shell builtin, so there's no need to execve, so the current () subshell exit as a shell, and trap is called.
Now, another weird thing is, if I do this: bash -c 'a=$(trap "echo trapped" EXIT && /bin/echo hi); echo $a'
, you will see that it is trapped!
I guess this is because bash does shortcut only in interactive mode. Another example difference between batch mode and interactive mode is for x in $(seq 1 30); sleep 1; done
. If you input it in the terminal, and press C-z immediately, and use fg
to bring it back, you will see that it will exit immediatly -- the remaining sleeps are skipped. If you put it in a script, and C-z, fg, it will continue to sleep for the remaining loops.