Impossible d'arrêter un script bash avec Ctrl + C

J'ai écrit un script bash simple avec une boucle pour imprimer la date et ping sur une machine distante:

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

Quand je le lance à partir d'un terminal je ne peux pas l'arrêter avec Ctrl + C. Il semble qu'il envoie le ^ C au terminal, mais le script ne s'arrête pas.

 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-sortingp 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 

Peu importe combien de fois j'appuie dessus ou à quelle vitesse je le fais. Je ne suis pas capable de l'arrêter.
Faites le test et réalisez vous-même.

En tant que solution secondaire, je l'arrête avec Ctrl + Z , qui l'arrête, puis kill %1 .

Qu'est-ce qui se passe exactement ici avec ^ C ?

Ce qui se passe est que bash et ping reçoivent le SIGINT ( bash n'étant pas interactif, ping et bash s'exécutent dans le même groupe de process créé par le shell interactif que vous avez exécuté).

Cependant, bash gère ce SIGINT de manière asynchronous, seulement après la sortie de la command en cours d'exécution. bash ne sort qu'à la réception de ce SIGINT si la command en cours d'exécution meurt d'un SIGINT (c'est-à-dire que son état de sortie indique qu'il a été tué par SIGINT).

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

Au-dessus, bash , sh et sleep reçoivent SIGINT quand j'appuie sur Ctrl-C, mais sh sort normalement avec un code de sortie 0, donc bash ignore le SIGINT, c'est pourquoi nous voyons "here".

ping , au less celui d'iputils, se comporte comme ça. Lorsqu'elle est interrompue, elle imprime des statistics et quitte avec un statut de sortie 0 ou 1 selon que ses pings ont été ou non répondus. Ainsi, lorsque vous appuyez sur Ctrl-C alors que la command ping est en cours d'exécution, bash note que vous avez appuyé sur Ctrl-C dans ses gestionnaires SIGINT, mais depuis que la command ping terminée normalement, bash ne quitte pas.

Si vous ajoutez un sleep 1 dans cette boucle et appuyez sur Ctrl-C pendant que le sleep est en cours d'exécution, parce que le sleep n'a pas de gestionnaire spécial sur SIGINT, il va mourir et signaler à bash qu'il est mort d'un SIGINT, et dans ce cas bash quittera il va se tuer avec SIGINT afin de signaler l'interruption à son parent).

Quant à savoir pourquoi bash se comporte comme ça, je ne suis pas sûr et je note que le comportement n'est pas toujours déterministe. Je viens de poser la question sur la list de diffusion de développement de bash ( Mise à jour : @Jilles a maintenant cloué la raison dans sa réponse ).

Le seul autre shell que j'ai trouvé qui se comporte de la même façon est ksh93 (Update, comme mentionné par @Jilles, FreeBSD sh ). Là, SIGINT semble être clairement ignoré. Et ksh93 sort quand une command est tuée par SIGINT.

Vous obtenez le même comportement que bash ci-dessus mais aussi:

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

Ne sort pas "test". C'est-à-dire qu'il sort (en se tuant avec SIGINT là) si la command attendait les meurtres de SIGINT, même si elle-même n'a pas reçu ce SIGINT.

Un travail autour serait d'append un:

 trap 'exit 130' INT 

En haut du script pour forcer bash à sortir lors de la réception d'un SIGINT (notez que dans tous les cas, SIGINT ne sera pas traité de façon synchrone, seulement après la sortie de la command en cours).

Idéalement, nous voudrions signaler à notre parent que nous sums morts d'un SIGINT (de sorte que si c'est un autre script bash par exemple, ce script bash est également interrompu). Effectuer une exit 130 n'est pas la même chose que mourir de SIGINT (bien que certains shells définiront $? même valeur pour les deux cas), mais il est souvent utilisé pour signaler un décès par SIGINT.

Cependant pour bash , ksh93 ou FreeBSD sh , cela ne fonctionne pas. Ce statut de sortie 130 n'est pas considéré comme un décès par SIGINT et un script parent n'abandonnerait pas là.

Donc, une meilleure alternative serait de se tuer avec SIGINT à la réception de SIGINT:

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

L'explication est que bash implémente WCE (wait and cooperative exit) pour SIGINT et SIGQUIT par http://www.cons.org/cracauer/sigint.html . Cela signifie que si bash reçoit SIGINT ou SIGQUIT en attendant la fin d'un process, il attendra la sortie du process et se quittera si le process s'est arrêté sur ce signal. Cela garantit que les programmes qui utilisent SIGINT ou SIGQUIT dans leur interface user fonctionneront comme prévu (si le signal n'entraîne pas la fin du programme, le script continuera normalement).

Un inconvénient apparaît avec les programmes qui capturent SIGINT ou SIGQUIT mais se terminent à cause de cela mais en utilisant une sortie normale () au lieu de renvoyer le signal à eux-mêmes. Il peut ne pas être possible d'interrompre les scripts qui appellent de tels programmes. Je pense que la vraie solution est dans de tels programmes tels que ping et ping6.

Un comportement similaire est implémenté par ksh93 et ​​/ bin / sh de FreeBSD, mais pas par la plupart des autres shells.

Comme vous le supposez, cela est dû au fait que le SIGINT est envoyé au process subordonné, et que le shell continue après que ce process se soit terminé.

Pour mieux gérer cela, vous pouvez vérifier l'état de sortie des commands en cours d'exécution. Le code de return Unix code à la fois la méthode par laquelle un process est sorti (appel système ou signal) et quelle valeur a été transmise à exit() ou quel signal a mis fin au process. Tout cela est assez compliqué, mais le moyen le plus rapide de l'utiliser est de savoir qu'un process qui a été interrompu par un signal aura un code de return différent de zéro. Ainsi, si vous vérifiez le code de return dans votre script, vous pouvez vous quitter si le process fils a été interrompu, supprimant le besoin d'inélégations comme les appels de sleep inutiles. Un moyen rapide de le faire tout au long de votre script est d'utiliser set -e , même si cela peut nécessiter quelques modifications pour les commands dont le statut de sortie est un non attendu.

Le terminal remarque le contrôle-c et envoie un signal INT au groupe de process de premier plan, qui inclut ici le shell, car ping n'a pas créé un nouveau groupe de process de premier plan. Ceci est facile à vérifier en interceptant INT .

 #! /bin/bash trap 'echo oh, I am slain; exit' INT while true; do ping -c5 127.0.0.1 done 

Si la command en cours d'exécution a créé un nouveau groupe de process au premier plan, alors le contrôle-c ira à ce groupe de process et non au shell. Dans ce cas, le shell devra inspecter les codes de sortie, car il ne sera pas signalé par le terminal.

(La manipulation d' INT dans les coquilles peut être fabuleusement compliquée, d'ailleurs, comme la coquille a parfois besoin d'ignorer le signal, et parfois non.) Source de plongée si curieux, ou réfléchir: tail -f /etc/passwd; echo foo )

Eh bien, j'ai essayé d'append un sleep 1 au script bash, et bang!
Maintenant, je suis capable de l'arrêter avec deux Ctrl + C.

Lorsque vous appuyez sur Ctrl + C , un signal SIGINT est envoyé au process en cours d'exécution, quelle command a été exécutée à l'intérieur de la boucle. Ensuite, le process subshell poursuit l'exécution de la command suivante dans la boucle, qui démarre un autre process. Pour pouvoir arrêter le script, il faut envoyer deux signaux SIGINT , l'un pour interrompre la command en cours d'exécution et l'autre pour interrompre le process du sousshell .

Dans le script sans l'appel de sleep , en appuyant sur Ctrl + C très rapidement et plusieurs fois ne semble pas fonctionner, et il n'est pas possible de quitter la boucle. Je suppose que presser deux fois n'est pas assez rapide pour le faire juste au bon moment entre l'interruption du process exécuté en cours et le début du suivant. Chaque pression sur Ctrl + C enverra un SIGINT à un process exécuté à l'intérieur de la boucle, mais pas au sousshell .

Dans le script avec sleep 1 , cet appel interrompt l'exécution pendant une seconde et lorsqu'il est interrompu par le premier Ctrl + C (premier SIGINT ), le sousshell prendra plus de time pour exécuter la command suivante. Alors maintenant, le second Ctrl + C (deuxième SIGINT ) ira au sousshell , et l'exécution du script se terminera.

Essaye ça:

 #!/bin/bash while true; do echo "Ctrl-c works during sleep 5" sleep 5 echo "But not during ping -c 5" ping -c 5 127.0.0.1 done 

Maintenant, changez la première ligne en:

 #!/bin/sh 

et essayez à nouveau – voir si le ping est maintenant interruptible.

 pgrep -f process_name > any_file_name sed -i 's/^/kill /' any_file_name chmod 777 any_file_name ./any_file_name 

par exemple pgrep -f firefox grep le PID de l'exécution de firefox et va save ce PID dans un file appelé any_file_name . La command 'sed' appenda le kill au début du numéro PID dans le file 'any_file_name'. Troisième ligne: any_file_name file exécutable. Maintenant, la ligne va tuer le PID disponible dans le file any_file_name . Ecrire les quatre lignes ci-dessus dans un file et exécuter ce file peut faire le contrôleC. Travailler absolument bien pour moi.

Vous êtes victime d'un bug bash bien connu. Bash fait jobcontrol pour les scripts qui est une erreur.

Ce qui se passe est que bash exécute les programmes externes dans un groupe de process différent de celui utilisé pour le script lui-même. Comme le groupe de process TTY est défini sur le groupe de process du process de premier plan en cours, seul ce process de premier plan est tué et la boucle du script shell continue.

Pour vérifier: Récupérez et comstackz un shell Bourne récent qui implémente pgrp (1) en tant que programme embedded, puis ajoutez un / bin / sleep 100 (ou / usr / bin / sleep selon votre plate-forme) Bourne Shell. Après avoir utilisé ps (1) pour get les ID de process pour la command sleep et le bash qui exécute le script, appelez pgrp <pid> et remplacez "<pid>" par l'ID du process et le bash qui exécute le script . Vous verrez différents ID de groupe de process. Maintenant appelez quelque chose comme pgrp < /dev/pts/7 (remplacez le nom tty par le tty utilisé par le script) pour get le groupe de process tty actuel. Le groupe de process TTY est égal au groupe de process de la command sleep.

Pour réparer: utilisez un autre shell.

Les sources Bourne Shell récentes sont dans mon package schily tools que vous pouvez find ici:

http://sourceforge.net/projects/schilytools/files/