2

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.

Mikko Rantalainen
  • 14,132
  • 10
  • 74
  • 112

3 Answers3

4

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.

MadScientist
  • 92,819
  • 9
  • 109
  • 136
  • Technically the Ctrl+C will be sent to the script, not to the `make` script. Additional testing shows that if I `trap` the SIGINT in `test.sh` I can *delay* the `SIGINT` as long as I want. However, even if `test.sh` later calls `exit 0` the final result is not returning from the shell with exit value 0 but SIGINT at that time. As a result, there seems to be no way to NOT have `make` to see SIGINT sooner or later. – Mikko Rantalainen Jan 27 '21 at 16:56
  • 2
    The SIGINT is always sent to make immediately, as I said. The effect you're seeing is because when make receives the signal, it doesn't exit immediately instead it waits for its child processes to die and cleans up after them, before exiting itself. So if you delay the exit of your script, that will delay the exit of make as well. But, make already got the signal and _will_ die. – MadScientist Jan 27 '21 at 17:01
  • I stand corrected. I found a good explanation of the whole process from "man bash" section "Signals". The bash documentation uses "current terminal process group ID" to describe consept that is sometimes just called process group. And when *terminal* receives Ctrl+C it will automatically send SIGINT to all process in the foreground process group. The important part is "If bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes." – child cannot prevent delivery of signal, only delay processing. – Mikko Rantalainen Jan 27 '21 at 19:55
  • It turns out this can be done after all, see my answer below: https://stackoverflow.com/a/65935205/334451 – Mikko Rantalainen Jan 28 '21 at 10:26
1

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>
永劫回帰
  • 652
  • 10
  • 21
0

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:

Mikko Rantalainen
  • 14,132
  • 10
  • 74
  • 112