What happens is that both
ping receive the SIGINT (
bash being not interactive, both
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).
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'
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
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).
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 - INT # restore default INT handler
kill -s INT "$$"