3

When i use 'trap' combined with select loop, namely when i try to hit CTRL+C to break out while options are displayed, it will just print ^C in terminal. If i remove 'trap' from script it will normally exit out, that is it will accept CTRL+C.

I've tested this on two different versions of bash (One shipped with CentOS and one shipped with Fedora), and i have an issue with the one from Fedora (4.4.23(1)-release). Bash version 4.2.46(2)-release that is shipped with CentOS seems to work fine. I've also tested this on local terminal and remote (via ssh). And problem is always on Fedora side.

I will post code to see what I'm talking about

This one doesn't work:

#!/bin/bash

trap exit SIGINT

select opt in One Two Three; do
        break
done

If i were to remove the entire 'trap exit SIGINT' line, it will work fine and accept CTRL+C without issues.

Any ideas how to fix or bypass this ?

Rui F Ribeiro
  • 55,929
  • 26
  • 146
  • 227
Marko Todoric
  • 357
  • 2
  • 17
  • Is `break` specified to leave `select` command? – Cyrus Apr 19 '19 at 20:53
  • This sounds like the `while true ; do cat ; done` example in the bug report [sr #108721: SIGINT trap not executing properly when commands interrupted; strange interaction with loops.](https://savannah.gnu.org/support/?108721) which I cannot reproduce on bash 4.4.12(1)-release. Also your example works fine here (trap is executed). – Freddy Apr 19 '19 at 21:21
  • @Cyrus yeah, just wanted to make code look smaller than it originally is. I kept on reducing it to figure out where the problem lies and i figured out it's nothing about my scripting methods, but it relates to 'trap'. So i didn't want to put a longer version of code here since it doesn't seem to matter. Original code will check against every response, filter out the correct one and then break out. Freddy, it does sound like a bug to me too. So I'll just check for updates to bash then :) – Marko Todoric Apr 20 '19 at 11:18

2 Answers2

6

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.

2

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.

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • That's not convincing; if `select` is a command, then `read` is a command too, but ^C will break out of a `read` but not out of a `select`. See also https://stackoverflow.com/a/53424823/10306503 –  Apr 20 '19 at 01:02
  • @mosvy Your answer on SO seems to say that `SIGINT` _won't_ break out of `read` (as in cancel) when a trap handler is installed for that signal, which is what I also have observed. The difference between `select` and `read` seems to be that the trap handler is executed asynchronously with `read` while it's executed after input to `select`. – Kusalananda Apr 20 '19 at 06:54
  • It will break out if it calls exit ;-) –  Apr 20 '19 at 06:55
  • @mosvy Yes, if the trap handler calls `exit`, obviously. What I meant was that the act of catching the signal does not cancel the `read`. – Kusalananda Apr 20 '19 at 07:01
  • @mosvy I wonder if the difference is that `read` is a simple command while `select` is a compound command? Hmm... too much handwaving, I'll just ask on `help-bash`. – Kusalananda Apr 20 '19 at 07:10