trapping SIGINT, SIGQUIT in asynchronous tasks

Scattered in bash documentation lies the truth about SIGINT and SIGQUIT signals being mysteriously ignored and non-trappable in background processes.

This has puzzled many, and will probably continue to do so.

Traps

Here's my try to unveil the terrible truth.

Let this script separate all cases and enlighten us:

cat <<'EOF' > /tmp/test.sh
#!/bin/bash
traps_show_and_test() {
    local label="$1"
    echo; echo "TRAPS from $label:"; trap
    trap "echo modified by $label" SIGINT  ## <-- MODIFICATION
    echo "TRAPS from $label (after modification attempt):"; trap
}
export -f traps_show_and_test

traps_show_and_test main
{ traps_show_and_test subshell; }
( traps_show_and_test subparen )
bash -c 'traps_show_and_test bash_subprocess'

traps_show_and_test job & wait
{ bash -c 'traps_show_and_test subshell_job'; } & wait
( bash -c 'traps_show_and_test subparen_job' ) & wait
bash -c 'traps_show_and_test bash_job' & wait

echo
echo FINAL main traps:
trap

EOF
chmod +x /tmp/test.sh
/tmp/test.sh

Running the previous script in bash (version is 4.3.46(1)-release) gives the following:

TRAPS from main:
TRAPS from main (after modification attempt):
trap -- 'echo modified by main' SIGINT

TRAPS from subshell:
trap -- 'echo modified by main' SIGINT
TRAPS from subshell (after modification attempt):
trap -- 'echo modified by subshell' SIGINT

TRAPS from subparen:
trap -- 'echo modified by subshell' SIGINT
TRAPS from subparen (after modification attempt):
trap -- 'echo modified by subparen' SIGINT

TRAPS from bash_subprocess:
TRAPS from bash_subprocess (after modification attempt):
trap -- 'echo modified by bash_subprocess' SIGINT

TRAPS from job:
trap -- 'echo modified by subshell' SIGINT
TRAPS from job (after modification attempt):
trap -- 'echo modified by job' SIGINT

TRAPS from subshell_job:
TRAPS from subshell_job (after modification attempt):
trap -- 'echo modified by subshell_job' SIGINT

TRAPS from subparen_job:
TRAPS from subparen_job (after modification attempt):
trap -- 'echo modified by subparen_job' SIGINT

TRAPS from bash_job:
trap -- '' SIGINT
trap -- '' SIGQUIT
TRAPS from bash_job (after modification attempt):
trap -- '' SIGINT
trap -- '' SIGQUIT

FINAL main traps:
trap -- 'echo modified by subshell' SIGINT

Let's highlight important points:

  • Foreground Jobs
    • subshells from {..} and (..) inherit their traps from the main scope.
    • only {..} can modify the main scope.
    • subprocesses do not inherit its trap from its parent process
    • they all can modify at least locally their traps
  • Background Jobs
    • bash function inherits its traps from main scope but can't modify it (like (..))
    • processes in {..} & and (..) & do not inherit traps
    • direct background subprocess have default signed traps that are umodifiable

Bash documentation

As a reference, here are the documentation from bash related to these behaviors:

Process group id effect on background process (in Job Control section of doc):

[...] processes whose process group ID is equal to the current terminal process group ID [..] receive keyboard-generated signals such as SIGINT. These processes are said to be in the foreground. Background processes are those whose process group ID differs from the terminal's; such processes are immune to keyboard-generated signals.

Default handler for SIGINT and SIGQUIT (in Signals section of doc):

Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

and about modification of traps (in trap builtin doc):

Signals ignored upon entry to the shell cannot be trapped or reset.