I found a way to make this work. It's a bit tricky so I'll explain the solution first. The important thing to understand that Ctrl+C is handled by your terminal and not by the currently running process in the terminal as I previously thought. When the terminal catches your Ctrl+C it will check the foreground process group and then send SIGINT
to all processes in that group immediately. When you run something via Makefile and press Ctrl+C the SIGINT
be immediately sent to Makefile and all processes that it started because those all belong in the foreground process group. And GNU Make handles SIGINT
by waiting for any currently executed child process to stop and then exit with a message
Makefile:<line number>: recipe for target '<target>' failed
make: *** [<target>] Interrupt
if the child exited with non-zero exit status. If child handled the SIGINT
by itself and exited with status 0, GNU Make will exit silently. Many programs exit via status code 130 on SIGINT
but this is not required. In addition, kernel and wait()
C API interface can differentiate with status code 130 and status code 130 + child received SIGINT
so if Make wanted to behave different for these cases, it would be possible regardless of exit code. bash
doesn't support testing for child process SIGINT
status but only supports exit status codes.
The solution is to setup processes so that your foreground process group does not include GNU Make while you want to handle Ctrl+C specially. However, as POSIX doesn't define a tool to create any process groups, we have to use bash
specific trick: use bash job control to trigger bash
to create a new process group. Be warned that this causes some side-effects (e.g. stdin and stdout behaves slightly different) but at least for my case it was good enough.
Here's how to do it:
I have following Makefile
(as usual, nested lines must have TAB instead of spaces):
test:
bash -c 'set -m; bash ./test.sh'
echo OK
and the test.sh
contains
#!/bin/bash
int_handler()
{
printf "\nReceived SIGINT, quitting...\n" 1>&2
exit 0
}
trap int_handler INT
while read -p "Enter some text or press Ctrl+C to exit > " input
do
echo "Your input was: $input"
done
The set -m
triggers creating a new foreground process group and the int_handler
takes care of returning successful exit code on exit. Of course, if you want to have some other exit code but zero on Ctrl+C, feel free to any any value suitable. If you want to have something shorter, the child script only needs trap 'exit 0' INT
instead of separate function and setup for it.
For additional information: