Bash ignoring SIGINT trap when 'select' loop is running

Any ideas how to fix or bypass this ?

You can bypass it by turning on the posix mode, either with the --posix option, or temporarily with set -o posix:

set -o posix
select opt in foo bar baz; do
    echo "opt=$opt"
done
set +o posix

For an explanation for this behavior, you can look at the zread() function, which is used by the read builtin (which is also called internally by bash in select):

  while ((r = read (fd, buf, len)) < 0 && errno == EINTR)
    /* XXX - bash-5.0 */
    /* We check executing_builtin and run traps here for backwards compatibility */
    if (executing_builtin)
      check_signals_and_traps ();   /* XXX - should it be check_signals()? */
    else
      check_signals ();

For some special reason, the executing_builtin is only set when the read builtin is called explicitly, not when it's called by select. This very much looks like a bug, not something deliberate.

When running in posix mode, a signal will cancel the read builtin. In that case, zreadintr() is called, which unlike zread(), is not re-calling the interrupted read(2) syscall after running the traps. See builtins/read.def:

      if (unbuffered_read == 2)
        retval = posixly_correct ? zreadintr (fd, &c, 1) : zreadn (fd, &c, nchars - nr);
      else if (unbuffered_read)
        retval = posixly_correct ? zreadintr (fd, &c, 1) : zread (fd, &c, 1);
      else
        retval = posixly_correct ? zreadcintr (fd, &c) : zreadc (fd, &c);

More details about bash's "restarting" read builtin here.


The relevant section from the bash manual is (I believe; at least this is what it behaves like) this:

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.

So your trap handler will not be called until the body of the select loop executes, because bash is waiting for the command to complete. Once the input has been received by select, the trap handler will execute.

The following modified script illustrates that better:

#!/bin/bash

trap 'echo INT;exit' SIGINT

select opt in One Two Three; do
    printf 'Got %s (%s)\n' "$REPLY" "$opt"
done

Running it (with bash 5.0.3), selecting 1, pressing Ctrl+C then Enter, and then selecting 3.

$ bash script.sh
1) One
2) Two
3) Three
#? 1
Got 1 (One)
#? ^C
1) One
2) Two
3) Three
#? 3
INT

The trap handler is executed when the current input (3) has been accepted and the before the body of the select loop would have been executed.

The trap handler is not executed when I press Enter after Ctrl+C because pressing Enter at the select prompt just re-displays the menu.