5

Consider the following minimal/arbitrary Bash script, which just slowly writes dots to stdout, until CTRLC is sent...

ctrlc.sh
#!/usr/bin/env bash

declare -i have_ctrlc=0

function handler() {
  printf 'SIGINT received\n'
  have_ctrlc=1
}

trap handler SIGINT

while [[ "$have_ctrlc" -eq 0 ]]; do
  printf '...'
  sleep 1
done

printf 'Done\n'

This script works as advertised interactively -- dots are written until CTRLC is pressed, then SIGINT received is printed, then Done is printed.

Now consider the following "wrapper" script, which itself calls ./ctrlc.sh with an arbitrary set of arguments and tees the output to a file...

wrapper1.sh
#!/usr/bin/env bash

./ctrlc.sh with these arbitrary args | tee -a "${0}.out"

When this script is run, dots are printed, but pressing CTRLC aborts the child script before SIGINT received and Done are printed, and I can't seem to find any way to get those to print.

I have tried:

  1. Changing the wrapper to invoke ctrlc.sh as a background job and waiting on that:

    ./ctrlc.sh with these arbitrary args | tee -a "${0}.out" &
    wait
    

    In this form, CTRLC ends the wrapper, but the dots continue to print in the background I manually/explicitly pkill -f ctrlc the child process.

  2. Changing the wrapper to invoke ctrlc.sh as a background job, but having the wrapper trap CTRLC itself/as well, and send SIGINT onto the child (process substitution is used so I can get the PID of ctrlc.sh instead of tee):

    ./ctrlc.sh with these args > >(tee -a "${0}.out") &
    trap "kill -INT '$!' && printf 'Killed %s\n' '$!'" SIGINT
    wait
    
  3. Changing ctrlc.sh to trap SIGUSR1 or SIGHUP, and changing the wrapper to send either of those onto the background job:

    trap handler SIGHUP
    
    ./ctrlc.sh with these args > >(tee -a "${0}.out") &
    trap "kill -HUP '$!' && printf 'Killed %s\n' '$!'" SIGINT
    wait
    

Both #2 and #3 behave the same way: when CTRLC is pressed, the Killed 12345 message from wrapper.sh is shown and the dots stop printing, but the SIGINT received and Done from the child ctrlc.sh are still not shown.

What should I be doing instead?

For the avoidance of doubt, I need the while loop to be abortable (e.g. with CTRLC), and the actions after the while to be run as well. With minimal changes to the child ctrlc.sh script (the real script is much more complex than the minimal version).

(Bash version happens to be v4.3.33, which I appreciate is very old)

2
  • 2
    Have you checked your .out file? Considering sending the SIGINT received message to stderr if isn't supposed to be part of the "product output".
    – konsolebox
    Commented Jun 4 at 5:30
  • This is an exemplary question - you've perfectly stripped the problem down to exactly the code needed to reproduce and address the problem, with nothing extraneous. Well done! Commented Jun 5 at 8:25

2 Answers 2

16
./ctrlc.sh with these arbitrary args | tee -a "${0}.out"

Upon SIGINT ./ctrlc.sh does properly execute the trap. The thing is the trap prints to its stdout which is connected to tee here, then tee prints to the terminal or whatever. You cannot see SIGINT received because tee exits (because of SIGINT) before SIGINT received gets to it*.

You can make tee ignore SIGINT:

./ctrlc.sh with these arbitrary args | (trap '' INT; exec tee -a -- "${0}.out")

(Note there is a more convenient way I was not aware of while writing the first version of my answer: tee -i. Give credit to this other answer, I learned from it.)

The above tee will not exit upon SIGINT; it will exit when there is nothing more to read, so after processing all its input.

Another "issue" is SIGINT received looks like a diagnostic message; for this reason consider printing it to stderr (printf 'SIGINT received\n' >&2 inside the trap). You will see messages printed to stderr even if you allow tee to exit too early, because stderr of ./ctrlc.sh does not go through tee. But this also means that messages printed to stderr will not go to the file tee appends to. So it's about whether or not you want messages generated after SIGINT to go to the file. If you do want them to go to the file, then you need to pipe them to tee and you must not allow tee to exit because of SIGINT.


* I suspect there may be a race condition and it may happen tee gets the signal significantly later. Even if it may happen, you cannot count on it; you need to assume tee exits because of SIGINT before SIGINT received gets to it.

2
  • Spot on. ./child | (trap...; tee...) was clearly the right answer here. I knew I was missing something fundamental (i.e. that the stdout I was seeing was being written to screen by tee, not by my script). Thanks. (PS wrt stdout vs stderr, this was just a minimal reproduction; my real script sends a bunch of logging to stderr already, including the "SIGINT received" message, it's just the results of my long operation going to stdout -- the long-op was collecting and sorting, the printing always happens afterward, but I wanted to print the results, regardless of the cancellation...) Commented Jun 5 at 4:22
  • I'm the author of the other answer, and I would like to point out that, while my answer works for tee, this answer will work for any program that doesn't go out of its way to prevent it. As such, I also endorse this answer, +1.
    – David G.
    Commented Jun 8 at 14:46
4
+100

tee needs to ignore interrupts. tee can ignore interrupts, with the -i option. Write:

./ctrlc.sh with these arbitrary args | tee -ia "${0}.out"

Edit: Stéphane Chazelas notes that, if "${0}" can start with a dash, you need a -- before it to prevent option handling. I tried for a minimal change from OP's example.

3
  • +1 for the option to tee; it's great that the tee authors considered this issue, because the (trap; tee) is a little tortuous. I've left the accepted answer as is, as it explains exactly what I needed to understand, but I've cetainly upvoted yours as it's exactly what I will change my script to do, if my version of tee has that flag. Thanks Commented Jun 5 at 17:32
  • 1
    Seeing as the -i option was there in version 7 Unix, it should be.
    – David G.
    Commented Jun 5 at 18:18
  • I'm the author of the other answer, I was not aware of -i. Now I see the option is in the POSIX specification of tee. I fully endorse your answer, +1 from me. Commented Jun 6 at 4:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .