Makefile: how to run bash script and ignore its exit status?

Viewed 582

Minimized test case for the problem:

I have following Makefile:

test:
    bash test.sh || true
    echo OK

and the test.sh contains

#!/bin/bash
while read -p "Enter some text or press Ctrl+C to exit > " input
do
    echo "Your input was: $input"
done

When I run make test and press Ctrl+C to exit the bash read the make will emit

Makefile:2: recipe for target 'test' failed
make: *** [test] Interrupt

How can I tell make to ignore the exit status of the script? I already have || true after the script which usually is enough to get make to keep going but for some reason, the SIGINT interrupting the read will cause make to behave different for this case.

I'm looking for a generic answer that works for processes other than while read loop in bash, too.

3 Answers

This has nothing to do with the exit status of the script. When you press ^C you're sending an interrupt signal to the make program, not just to your script. That causes the make program to stop, just like ^C always does.

There's no way to have make ignore ^C operations; whenever you press ^C at the terminal, make will stop.

ctrl+c sends a signal to the program to tell it to stop. What you want is ctrl+d which sends the signal EOT (end of transmission). You will need to send ctrl+d twice unless you are at the beginning of a line.

some text<c-d><c-d>

or

some text<return>
<c-d>

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:

Related