There are several ways you can cut off the effect of Ctrl+C:
- Change the terminal setting so that it doesn't generate a signal.
- Block the signal so that it is saved for later delivery, when the signal becomes unblocked.
- Ignore the signal, or set a handler for it.
- Run subprocesses in a background process group.
Since you want to detect that Ctrl+C has been pressed, ignoring the signal is out. You could change the terminal settings, but then you would need to write custom key processing code. Shells don't provide access to signal blocking.
You can however isolate subprocesses from receiving the signal automatically by running them in a separate process group. Interactive shells run background commands in a separate process group by default, but non-interactive shells run them in the same process group, and all processes in the foreground process group receive a signal from terminal events. To tell the shell to run background jobs in a separate process group, run set -m
. Running setsid ping …
is another way of forcing ping
to run in a separate process group.
set -m
interrupted=
trap 'echo Interrupted, but ping may still be running' INT
set -m
ping … &
while wait; [ $? -ge 128 ]; do echo "Waiting for background jobs"; done
echo ping has finished
If you want Ctrl+Z to suspend a background process group, you'll need to propagate the signal from the shell.
Controlling signals finely is a bit of a stretch for a shell script, and shells other than ATT ksh tend to be a little buggy when you reach the corner cases, so consider a language that gives you more control such as Perl, Python or Ruby.
The 130 (128+SIGINT) you see in $?
after the last command died of a SIGINT is a simplified representation of its exit status made by some shells like bash
. Other shells will use different representations (like 256+signum in ksh93, 128+256+signum in yash, textual representations like sigint
or sigquit+core
in rc
/es
). See Default exit code when process is terminated? for more details on that.
A process can wait for its child process and query its status:
- if it was stopped (with which signal)
- if it was resumed
- if it was killed (with which signal)
- if it has trapped (for ptraced processes)
- if it dumped a core
- if it exited normally with the
_exit()
system call (with which exit code)
To do that, they use one of the wait()
, waitpid()
, waitid()
(see also obsolete wait3()
, wait4()
) or a handler on the SIGCHLD system call.
Those system calls return all that information above. (Except for waitid()
on some system, only the lowest 8 bits of the number passed to _exit()
for child that terminate normally are available though).
But bash
(and most Bourne-like and csh-like shells) bundle all that information in a 8 bit number for $?
($?
is the lowest 8 bits of the exit code for processes that terminate normally, and 128+signum it it was killed or suspended or trapped, all the other information is not available). So obviously, there's some information being lost. In particular, through $?
alone, one can't tell if a process did a _exit(130)
or died of a SIGINT.
bash
knows when a process is being killed obviously. For example when background processes are killed, you see:
[1]+ Interrupt sleep 20
But in $?
, it doesn't give you enough information to tell whether it was killed by SIGINT or it called _exit(130)
.
Since most shells do that transformation, applications know better than doing _exit(number_greater_than_127)
for anything but reporting a death by signal though.
Still if a process does a _exit(130)
, the process waiting for that process will detect that that process terminated normally, not that it was killed by a signal. In C, WIFEXITED()
will return true, WIFSIGNALED()
will return false.
bash
itself will not consider the process as having died of a SIGINT (even though it lets you think it might have through $?
containing the same value as if it had died of a SIGINT).
So, that will not trigger the special handling of SIGINT that bash
does. In a script, both bash
and the currently running command in the script will receive a SIGINT upon ^C
(as they're both in the same process group).
bash
dies upon receiving SIGINT only if the command it is waiting for also died of a SIGINT (the idea being that if for instance in your script, you run vi or less and use ^C to abort something there which doesn't make vi
/less
die, your script doesn't die upon returning quitting vi
/less
later on).
If that command bash
is waiting for does a _exit(130)
in a handler of SIGINT, bash
will not die upon that SIGINT (it will not consider itself as having been interrupted because it doesn't believe the child has been interrupted).
That's why when you want to report a death by SIGINT, that you have indeed been interrupted even though you are actually doing some extra processing upon receiving that signal in a handler, you should not do a _exit(130)
, but actually kill yourself with SIGINT (after having restored the default handler for SIGINT). In a shell, that's with:
trap '
extra processing
trap - INT # restore SIGINT handler
kill -s INT "$$" # report to the parent that we have indeed been
# interrupted
' INT
Best Answer
All you need to do is change the EXIT handler inside your cleanup handler. Here's an example: