Bash – Unable to stop a bash script with Ctrl+C

bashsignalsterminal

I wrote a simple bash script with a loop for printing the date and ping to a remote machine:

#!/bin/bash
while true; do
    #     *** DATE: Thu Sep 17 10:17:50 CEST 2015  ***
    echo -e "\n*** DATE:" `date` " ***";
    echo "********************************************"
    ping -c5 $1;
done

When I run it from a terminal I am not able to stop it with Ctrl+C.
It seems it sends the ^C to the terminal, but the script does not stop.

MacAir:~ tomas$ ping-tester.bash www.google.com

*** DATE: Thu Sep 17 23:58:42 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C                                                          <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms

*** DATE: Thu Sep 17 23:58:48 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms

No matter how many times I press it or how fast I do it. I am not able to stop it.
Make the test and realize by yourself.

As a side solution, I am stopping it with Ctrl+Z, that stops it and then kill %1.

What is exactly happening here with ^C?

Best Answer

What happens is that both bash and ping receive the SIGINT (bash being not interactive, both ping and bash run in the same process group which has been created and set as the terminal's foreground process group by the interactive shell you ran that script from).

However, bash handles that SIGINT asynchronously, only after the currently running command has exited. bash only exits upon receiving that SIGINT if the currently running command dies of a SIGINT (i.e. its exit status indicates that it has been killed by SIGINT).

$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere

Above, bash, sh and sleep receive SIGINT when I press Ctrl-C, but sh exits normally with a 0 exit code, so bash ignores the SIGINT, which is why we see "here".

ping, at least the one from iputils, behaves like that. When interrupted, it prints statistics and exits with a 0 or 1 exit status depending on whether or not its pings were replied. So, when you press Ctrl-C while ping is running, bash notes that you've pressed Ctrl-C in its SIGINT handlers, but since ping exits normally, bash does not exit.

If you add a sleep 1 in that loop and press Ctrl-C while sleep is running, because sleep has no special handler on SIGINT, it will die and report to bash that it died of a SIGINT, and in that case bash will exit (it will actually kill itself with SIGINT so as to report the interruption to its parent).

As to why bash behaves like that, I'm not sure and I note the behaviour is not always deterministic. I've just asked the question on the bash development mailing list (Update: @Jilles has now nailed down the reason in his answer).

The only other shell I found that behave similarly is ksh93 (Update, as mentioned by @Jilles, so does FreeBSD sh). There, SIGINT seems to be plainly ignored. And ksh93 exits whenever a command is killed by SIGINT.

You get the same behaviour as bash above but also:

ksh -c 'sh -c "kill -INT \$\$"; echo test'

Doesn't output "test". That is, it exits (by killing itself with SIGINT there) if the command it was waiting for dies of SIGINT, even if it, itself didn't receive that SIGINT.

A work around would be to do add a:

trap 'exit 130' INT

At the top of the script to force bash to exit upon receiving a SIGINT (note that in any case, SIGINT won't be processed synchronously, only after the currently running command has exited).

Ideally, we'd want to report to our parent that we died of a SIGINT (so that if it's another bash script for instance, that bash script is also interrupted). Doing an exit 130 is not the same as dying of SIGINT (though some shells will set $? to same value for both cases), however it's often used to report a death by SIGINT (on systems where SIGINT is 2 which is most).

However for bash, ksh93 or FreeBSD sh, that doesn't work. That 130 exit status is not considered as a death by SIGINT and a parent script would not abort there.

So, a possibly better alternative would be to kill ourself with SIGINT upon receiving SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
Related Question