5

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 &&.
    • If you use ;, or even || at any point, the trap will execute.
  2. All commands in the chain must execute.
    • If any one of the commands (except the last) exits with a non-zero exit status such that the last command never executes, the trap will execute.
  3. The final command must be a program on the system, not a shell builtin and not a function.
    • Non-final commands can be builtins or functions and the trap will not run as long as the final command is a program

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.

onlynone
  • 7,602
  • 3
  • 31
  • 50
  • Your observations are right, See [Why doesn't set -e (or set -o errexit, or trap ERR) do what I expected?](http://mywiki.wooledge.org/BashFAQ/105) – Inian Jun 07 '18 at 07:49
  • May be try adding stderr also to null, `> /dev/null 2>&1` – Inian Jun 07 '18 at 07:53
  • @Inian that definitely seems related, but my scenario doesn't depend on the error status of the commands (or whether they qualify as simple commands), but on whether the last command in a list is a builtin/function or system command. – onlynone Jun 07 '18 at 16:54
  • regarding redirecting stderr. That wouldn't help. The redirection of `cd` to `/dev/null` is actually unneeded and not part of the problem. The `EXIT` trap is being run after all the commands (after `cd` and after `pwd`). And it's writing to stdout. So there's no way to avoid it. Other than maybe closing stdout, or redirecting stdout to `/dev/null` after the last command in the subshell. However that still doesn't address why the trap is sometime being executed and sometimes not. – onlynone Jun 07 '18 at 16:58
  • Which Bash version were you using? I can't reproduce the weird behavior you described with Bash 5.0.17 – Rayman Jul 01 '22 at 08:04
  • I'm seeing it with 5.0.2 right now. I'll try a later version when I get a chance. – onlynone Jul 05 '22 at 18:41
  • Just tried on 5.1.16 and it prints `hi trapped` for all of them! I wonder when it changed. – onlynone Jul 05 '22 at 18:46

1 Answers1

3

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.

Bao Haojun
  • 966
  • 5
  • 9