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