What does "a signal for which a trap has been set" mean?
That's a signal for which a handler has been defined (with trap 'handling code' SIG) where the handling code is not empty as that would cause the signal to be ignored.
So signals that have their default disposition are not signals for which a trap has been set.
Some of that quote in your post also applies to signals that have their default disposition, though obviously not the part about running the trap, since no trap have been defined for them.
The manual talks about signal delivery to the shell, not to the commands you run from that shell.
1.
If Bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes.
(1)
why in my first example, does ctrl-C make the foreground job exit immediately before it can complete
If you run sleep 10 at the prompt of an interactive shell, the shell will put that job in the foreground (through an ioctl() on the tty device that tells the terminal line discipline which process group is the foreground one), so only sleep will get a SIGINT upon ^C and the interactive shell will not, so it's not useful to test that behaviour.
The parent shell, as it's interactive won't receive the SIGINT as its process is not in the foreground process group.
Each command is free to handle signals as they please. sleep doesn't do anything specially with SIGINT, so will get the default disposition (terminate) unless SIGINT was being ignored upon startup.
(2) If you run sleep 10 in a non-interactive shell,
bash -c 'sleep 10; echo "$?"'
Both the noninteractive bash shell and sleep will receive the SIGINT when you press Ctrl-C.
If bash exited straight away, it could leave the sleep command running unattended in background if it happened to ignore or handle the SIGINT signal. So instead,
bash like most other shells, blocks the reception of signals (at least some signals) when waiting for commands.
- Delivery is resumed after the command exits (upon which traps are executed). That also avoids commands in traps to run concurrently with other commands.
In that example above, sleep will die upon SIGINT, so bash doesn't have long to wait to handle its own SIGINT (here to die as I didn't add a trap on SIGINT).
(3) when you press Ctrl+C while running the noninteractive shell:
bash -c 'sh -c "trap \"\" INT; sleep 3"; echo "$?"'
(with no trap on SIGINT) bash is not killed by the SIGINT. bash, like a few other shells treat SIGINT and SIGQUIT specially. They implement the wait and cooperative exit behaviour described at https://www.cons.org/cracauer/sigint.html (and is known to cause a few annoyances like the scripts calling SIGINT handling commands that can't be interrupted with ^C)
(4) To properly test, you should run a non-interactive bash that has SIGINT trap set and calls a command that doesn't die straight away upon SIGINT like:
bash -c 'trap "echo Ouch" INT; sh -c "trap \"\" INT; sleep 3"'
bash is waiting for sh that has (along with sleep) SIGINT ignored (because of the trap "" INT), so SIGINT won't kill sleep nor sh. bash does not ignore SIGINT, but its handling is defered until sh returns. You see Ouch being displayed, not upon Ctrl+C, but after sleep and sh have terminated normally.
Note that the trap command sets a trap for a signal for the same shell in which it runs. So when the trap command is executed outside the noninteractive shell and in the parent shell,
$ trap "echo You hit control-C!" INT
$ bash -c 'sleep 10; echo "$?"'
^C
$
the noninteractive bash, and sleep commands won't inherit that trap from the parent shell. Signal handlers are lost upon executing a different command (execve() wipes the whole address space of the process including the code of the handler). Upon execve(), signals that had a handler defined revert to the default disposition, those that were ignored remain ignored.
In addition to that, in most shells, traps are also reset in sub-shells.
2.
When Bash is waiting for an asynchronous command via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status greater than 128, immediately after which the trap is executed.
When using wait explicitly, wait is interrupted by any signal that has a trap set (and obviously also those that kill the shell altogether).
That makes it difficult to get the exit status of a command reliably when there are trapped signals:
$ bash -c 'trap "echo Ouch" INT; sh -c "trap \"\" INT; sleep 10" & wait "$!"; echo "$?"'
^COuch
130
In that case, sleep and sh were not killed by the SIGINT (since they ignore it). Still wait returns with a 130 exit status because a signal (SIGINT) was received while it was waiting for sh. You'd need to repeat the wait "$!" until sh really terminates to get sh's exit status.