O que acontece é que ambos bash e ping recebem o SIGINT ( bash sendo não interativo, ping e bash são executados no mesmo grupo de processos que foi criado e configurado como o grupo de processos de primeiro plano do terminal pelo shell interativo do qual você executou o script.
No entanto, bash manipula SIGINT de forma assíncrona, somente depois que o comando em execução no momento é encerrado. bash só sai ao receber esse SIGINT se o comando atualmente em execução morrer de um SIGINT (ou seja, seu status de saída indica que ele foi eliminado pelo SIGINT).
$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere
Acima, bash , sh e sleep recebem SIGINT quando eu pressiono Ctrl-C, mas sh sai normalmente com um código de saída 0, então bash ignora o SIGINT, e é por isso que vemos "aqui".
ping , pelo menos o do iputils, comporta-se assim. Quando interrompido, imprime estatísticas e sai com um status de saída 0 ou 1, dependendo de seus pings terem sido respondidos ou não. Portanto, quando você pressiona Ctrl-C enquanto ping está em execução, bash observa que você pressionou Ctrl-C em seus manipuladores SIGINT, mas como ping sai normalmente, bash não sai.
Se você adicionar sleep 1 nesse loop e pressionar Ctrl-C enquanto sleep estiver em execução, porque sleep não tem manipulador especial no SIGINT, ele morrerá e reportará a bash que morreu de um erro SIGINT, e nesse caso bash irá sair (ele irá realmente se matar com SIGINT para relatar a interrupção para seu pai).
Quanto ao porquê bash se comporta assim, não tenho certeza e noto que o comportamento nem sempre é determinístico. Acabei de fazer uma pergunta à sobre a lista de discussão bash ( > Atualização : o @Jilles já descobriu o motivo em sua resposta .
O único outro shell que eu encontrei que se comporta de forma semelhante é o ksh93 (Update, mencionado pelo @Jilles, assim como o FreeBSD sh ). Lá, o SIGINT parece ser claramente ignorado. E ksh93 sai sempre que um comando é eliminado pelo SIGINT.
Você tem o mesmo comportamento que bash acima, mas também:
ksh -c 'sh -c "kill -INT \$\$"; echo test'
Não imprime "teste". Ou seja, ele sai (se matando com SIGINT) se o comando que estava esperando morrer de SIGINT, mesmo que ele próprio não tenha recebido essa SIGINT.
Uma solução seria adicionar um:
trap 'exit 130' INT
Na parte superior do script, para forçar o bash a sair ao receber um SIGINT (observe que, em qualquer caso, o SIGINT não será processado de forma síncrona, somente após o comando em execução no momento).
Idealmente, gostaríamos de relatar a nosso pai que morremos de um SIGINT (para que, se for outro script bash , por exemplo, esse script bash também seja interrompido). Fazer um exit 130 não é o mesmo que morrer de SIGINT (embora alguns shells definam $? para o mesmo valor para ambos os casos), mas é frequentemente usado para reportar uma morte por SIGINT (em sistemas onde o SIGINT é 2 que é mais ).
No entanto, para bash , ksh93 ou FreeBSD sh , isso não funciona. Aquele 130 status de saída não é considerado uma morte pelo SIGINT e um script pai não abortará lá.
Assim, uma alternativa possivelmente melhor seria nos matar com SIGINT ao receber o SIGINT:
trap '
trap - INT # restore default INT handler
kill -s INT "$$"
' INT