I am trying to handle SIGINT/CTRL+C interrupt in such a way that if a user accidentally presses ctrl-c, he is prompted with a message, "Do you wish to quit?(y/n)". If he enters yes, then exit the script. If no, then continue from where ever the interrupt occurred. Basically, I need Ctrl-C to work similar to Ctrl-Z/SINTSTP but in a slightly different way. I have tried various ways to acheieve this but I didn't get the expected results. Below are few scenario which I tried.
Case:1
Script : play.sh
#!/bin/sh
function stop()
{
while true; do
read -rep $'\nDo you wish to stop playing?(y/n)' yn
case $yn in
[Yy]* ) echo "Thanks for playing !!!"; exit 1;;
[Nn]* ) break;;
* ) echo "Please answer (y/n)";;
esac
done
}
trap 'stop' SIGINT
echo "going to sleep"
for i in {1..100}
do
echo "$i"
sleep 3
done
echo "end of sleep"
When I run the above script, I get the expected results.
Output:
$ play.sh
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$ play.sh
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$
Case:2
I moved the for loop to a new script loop.sh, thus play.sh becomes the parent process and loop.sh the child process.
Script : play.sh
#!/bin/sh
function stop()
{
while true; do
read -rep $'\nDo you wish to stop playing?(y/n)' yn
case $yn in
[Yy]* ) echo "Thanks for playing !!!"; exit 1;;
[Nn]* ) break;;
* ) echo "Please answer (y/n)";;
esac
done
}
trap 'stop' SIGINT
loop.sh
Script : loop.sh
#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
echo "$i"
sleep 3
done
echo "end of sleep"
Output in this case is not as expected.
Output:
$ play.sh
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!
$ play.sh
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$
I understand that when a process receives a SIGINT signal, it propagates the signal to all the child processes, thus my 2nd case is failing. Is there any way that I can avoid SIGINT being propagated to child processes and thus make the loop.sh work exactly the way it worked in the 1st case?
Note: This is just an example of my actual application. The application I am working on has several child scripts in play.sh and loop.sh. I should make sure that the application on receiving SIGINT, should not terminate but it should prompt the user with a message.
Best Answer
Great classic question about managing jobs and signals with good examples! I've developed a stripped down test script to focus on the mechanics of the signal handling.
To accomplish this, after starting the children (loop.sh) in the background, call
wait
, and upon receipt of the INT signal,kill
the process group whose PGID equals your PID.For the script in question,
play.sh
, this can be accomplished by the following:In the
stop()
function replaceexit 1
withStart
loop.sh
as a background process (multiple background processes can be started here and managed byplay.sh
)Add
wait
at the end of the script to wait for all children.When your script starts a process, that child becomes a member of a process group with PGID equal to the PID of the parent process which is
$$
in the parent shell.For example, the script
trap.sh
started threesleep
processes in the background and is nowwait
ing on them, notice the process group ID column (PGID) is the same as the PID of the parent process:In Unix and Linux you can send a signal to every process in that process group by calling
kill
with the negative value of thePGID
. If you givekill
a negative number, it will be used as -PGID. Since the script's PID ($$
) is the same as it's PGID, you cankill
your process group in the shell withyou have to give a signal number or name, otherwise some implementations of kill will tell you "Illegal option" or "invalid signal specification."
The simple code below illustrates all of this. It sets a
trap
signal handler, spawns 3 children, then goes into an endless wait loop, waiting to kill itself by thekill
process group command in the signal handler.Here's a sample run:
OK no extra processes running. Start the test script, interrupt it (
^C
), choose to ignore the interrupt, and then suspend it (^Z
):Check the running processes, note the process group numbers (
PGID
):Bring our test script to the foreground (
fg
) and interrupt (^C
) again, this time choose not to ignore:Check running processes, no more sleeping:
Note about your shell:
I had to modify your code to get it to run on my system. You have
#!/bin/sh
as the first line in your scripts, yet the scripts use extensions (from bash or zsh) which are not available in /bin/sh.