Unable to stop a bash script with Ctrl+C
Solution 1
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
Solution 2
The explanation is that bash implements WCE (wait and cooperative exit) for SIGINT and SIGQUIT per http://www.cons.org/cracauer/sigint.html. That means that if bash receives SIGINT or SIGQUIT while waiting for a process to exit, it will wait until the process exits and will exit itself if the process exited on that signal. This ensures that programs that use SIGINT or SIGQUIT in their user interface will work as expected (if the signal did not cause the program to terminate, the script will continue normally).
A downside appears with programs that catch SIGINT or SIGQUIT but then terminate because of it but using a normal exit() instead of by resending the signal to themselves. It may not be possible to interrupt scripts that call such programs. I think the real fix there is in such programs such as ping and ping6.
Similar behaviour is implemented by ksh93 and FreeBSD's /bin/sh, but not by most other shells.
Solution 3
The terminal notices the control-c and sends an INT
signal to the foreground process group, which here includes the shell, as ping
has not created a new foreground process group. This is easy to verify by trapping INT
.
#! /bin/bash
trap 'echo oh, I am slain; exit' INT
while true; do
ping -c5 127.0.0.1
done
If the command being run has created a new foreground process group, then the control-c will go to that process group, and not to the shell. In that case, the shell will need to inspect exit codes, as it will not be signalled by the terminal.
(INT
handling in shells can be fabulously complicated, by the way, as the shell sometimes needs to ignore the signal, and sometimes not. Source dive if curious, or ponder: tail -f /etc/passwd; echo foo
)
Solution 4
As you surmise, this is due to the SIGINT being sent to the subordinate process, and the shell continuing on after that process exits.
To handle this in a better way, you can check the exit status of the commands which are running. The Unix return code encodes both the method by which a process exited (system call or signal) and what value was passed to exit()
or what signal terminated the process. This is all rather complicated, but the quickest way of using it is to know that a process that was terminated by signal will have a non-zero return code. Thus, if you check the return code in your script, you can exit yourself if the child process was terminated, removing the need for inelegancies like unnecessary sleep
calls. A quick way to do this throughout your script is to use set -e
, though it may require a few tweaks for commands whose exit status is an expected nonzero.
Solution 5
Well, I tried to add a sleep 1
to the bash script, and bang!
Now I'm able to stop it with two Ctrl+C.
When pressing Ctrl+C, a SIGINT
signal is sent to the process currently executed, which command was run inside the loop. Then, the subshell process continues executing the next command in the loop, that starts another process.
To be able to stop the script it is necessary to send two SIGINT
signals, one to interrupt the current command in execution and one to interrupt the subshell process.
In the script without the sleep
call, pressing Ctrl+C really fast and many times does not seem to work, and it is not possible to exit the loop. My guess is that pressing twice is not enough fast to make it just in the right moment between the interruption of current executed process and the start of the next one. Every Ctrl+C pressed will send a SIGINT
to a process executed inside the loop, but neither to the subshell.
In the script with sleep 1
, this call will suspend the execution for one second, and when interrupted by the first Ctrl+C (first SIGINT
), the subshell will take more time to execute the next command. So now, the second Ctrl+C (second SIGINT
) will go to the subshell, and the script execution will end.
Related videos on Youtube

nephewtom
I am someone who enjoys life cares for family thrives with programming feel passion for music is in love with graphics and like sports. @Just love ;-)!
Updated on September 18, 2022Comments
-
nephewtom about 1 month
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?
-
Admin about 2 yearsI have a script that wouldn't let me
ctrl+c
out of asudo
password prompt. Just kept asking again & again. I put at the topdidSudo="$(sudo pwd)";
& nowctrl+c
stops the script. If I enter my password successfully, the rest of mysudo
commands work.
-
-
schily about 7 yearsSet -e does not work correctly in bash unless you are using bash-4
-
schily about 7 yearsYou are mistaken, on a correctly working shell, a single ^C is sufficient see my answer for the background.
-
schily about 7 yearsIn this case, the problem is not signal handling but the fact that bash does jobcontrol in the script although it should not, see my answer for more information
-
Stéphane Chazelas about 7 yearsWhat version of
bash
is that? AFAIKbash
only does that if you pass the -m or -i option. -
schily about 7 yearsIt seems that this does no longer apply to bash4 but when the OP has such problems, he seems to use bash3
-
Stéphane Chazelas about 7 yearsCan't reproduce with bash3.2.48 nor bash 3.0.16 nor bash-2.05b (tried with
bash -c 'ps -j; ps -j; ps -j'
). -
Tom Hunt about 7 yearsWhat's meant "does not work correctly"? I've used it on bash 3 successfully, but there's probably some edge cases.
-
schily about 7 yearsIn a few simple cases, bash3 did exit on error. This did however not happen in the general case. As a typical result, make did not stop when creating a target failed and this was from a makefile that worked on a list of targets in subdirectories. David Korn and I had to mail many weeks with the bash maintainer to convince him to fix the bug for bash4.
-
schily about 7 yearsThis definitely happens when you call bash as
/bin/sh -ce
. I had to add an ugly workaround intosmake
that explicitely kills the process group for a currently running command in order to permit^C
to abort a layered make call. Did you check whether bash changed the process group from the process group id it was initiated with? -
Stéphane Chazelas about 7 yearsNote that the problem here is that
ping
returns with a 0 exit status upon receiving SIGINT and thatbash
then ignores the SIGINT it received itself if that's the case. Adding a "set -e" or check the exit status won't help here. Adding an explicit trap on SIGINT would help. -
Stéphane Chazelas about 7 years
ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'
does report the same pgid for ps and bash in all 3 ps invocations. (ARGV0=sh iszsh
way to pass argv[0]). -
Stéphane Chazelas about 7 yearsFor the SIGINT to go to the new process group, the command would also have to do an ioctl() to the terminal to make it the foreground process group of the terminal.
ping
has no reason to start a new process group here and the version of ping (iputils on Debian) with which I can reproduce the OP's problem does not create a process group. -
schily about 7 yearsThis does not contain a loop as explained for the cause that forced me to implement the named workaround.
-
Stéphane Chazelas about 7 yearsSame with
ARGV0=sh ./bash -ce 'while ps -j; do ps -j;sleep 1; done'
. Can you reproduce the problem? -
Stéphane Chazelas about 7 yearsNote that it's not the terminal that sends the SIGINT, it's the line discipline of the tty device (the driver (code in the kernel) of the /dev/ttysomething device) upon receiving an unescaped (by lnext usually ^V) ^C character from the terminal.
-
schily about 7 yearsSorry, I have a workaround in
smake
for the problem and I believe it does not make sense to put effort into this problem. If you are interested in the problem, I encourage you to fetch the smake source, disable the workaround and then do tests with calling smake and typing ^C at top level. If you like to do that, better start with the schily tools, as there are more subdirectories in the loop. -
nephewtom about 7 yearsWell, considering you've been down voted, and currently your answer has score -1, I'm not very convinced I should take your answer seriously.
-
schily about 7 yearsThe fact that some people downvote is not always related to the quality of a reply. If you need to type two times ^c, you definitely are a victim of a bash bug. Did you try a different shell? Did you try the real Bourne Shell?
-
schily about 7 yearsIf the shell if working correctly, it runs everything from a script in the same process group and then a single ^c is sufficient.
-
nephewtom about 7 yearsWell, that's right, I agree that the fact that some people down voted does not mean anything. But I do not agree I'm mistaken, that is what you said in your first comment. If there is a bug in bash, that does not mean I am mistaken. It means there is a bug in bash terminal. Period. My surmise, as @TomHunt mentioned seems to be the actual bash behaviour.
-
Scott - Слава Україні about 7 yearsjilles’s answer explains the “why”. As an illustrative example, consider
for f in *.txt; do vi "$f"; cp "$f" newdir; done
. If the user types Ctrl+C while editing one of the files,vi
just displays a message. It seems reasonable that the loop should continue after the user finishes editing the file. (And yes, I know that you could sayvi *.txt; cp *.txt newdir
; I’m just submitting thefor
loop as an example.) -
Stéphane Chazelas about 7 yearsThanks, that makes a lot of sense. I note FreeBSD sh doesn't abort either when the cmd exits with exit(130) either, which is a common way to report the death by SIGINT of a child (mksh does a
exit(130)
for instance if you interruptmksh -c 'sleep 10;:'
). -
Stéphane Chazelas about 7 years@Scott, good point. Though
vi
(wellvim
at least) does disable ttyisig
when editing (it does not abviously when you run:!cmd
though, and that would very much apply in that case). -
terdon about 7 yearsWhile it is fine to mention your tools, you really should always make it clear that they are yours. Please make sure to mention that you're the author of the package you recommend so that users can make an informed choice.
-
schily about 7 yearsDo you believe I was not obvious enough? Would you tell David Korn to mention that he wrote the Korn Shell when he is writing about it? I expect a minimum level experience of life and assume that people are able to recognize that there is a coincidence in names.
-
schily about 7 yearsDo you believe it is helpful to describe incorrect behavior without mentioning that it is incorrect?
-
nephewtom about 7 yearsI think it is fine to mention that there is a bug in bash. But I don't think it is right to say I was mistaken. I just described what it is really happening. I am not saying that mine or @TomHunt should be the perfect fix. Anyway, as with life, computers and software are not perfect, and sometimes a temporal fix is just enough. What I do not find reasonable is that for this bug (currently without many implications at least for me), you suggest to fix it using a different shell. Come on!
-
schily about 7 yearsIf there has been evidence that bash bugs are fixed on a regular base, I of course would have recommended to make a bug report.
-
Jonathan Hartley about 7 yearsThe behaviour @nephewtom describes in this answer can be explained by different commands in the script behaving differently when they receive Ctrl-C. If a sleep is present, it's overwhelmingly likely that Ctrl-C will be received while the sleep is executing (assuming everything else in the loop is fast). The sleep is killed, with exit value 130. The parent of sleep, a shell, notices that sleep was killed by sigint, and exits. But if the script contains no sleep, then the Ctrl-C goes to ping instead, which reacts by exiting with 0, so the parent shell carries on executing the next command.
-
Stéphane Chazelas about 5 years@Tim, see my edit for correction on your edit.
-
Tim almost 4 years@StéphaneChazelas Thanks. So it is because
ping
exits with 0 after receiving SIGINT. I found a similar behavior when a bash script containssudo
instead ofping
, butsudo
exits with 1 after receiving SIGINT. unix.stackexchange.com/questions/479023/…