bashshellrvm

Why does the EXIT trap in bash subshells not always get called?


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:

  1. The entirety of the subshell program is a list of commands chained together with &&.
  1. All commands in the chain must execute.
  1. The final command must be a program on the system, not a shell builtin and not a function.

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.


Solution

  • 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.