How to pipe a bash command and keep Ctrl+C working?

Ctrl+C causes a SIGINT to be sent to all processes in the pipeline (as they're all run in the same process group that correspond to that foreground job of your interactive shell).

So in:

loopHelloWorld | 
  while IFS= read -r line; do
    echo "$line"
  done

Both the process running loopHelloWorld and the one running the subshell that runs the while loop will get the SIGINT.

If loopHelloWorld writes the Ctrl+C to nicely shutdown message on its stdout, it will also be written to the pipe. If that's after the subshell at the other end has already died, then loopHelloWorld will also receive a SIGPIPE, which you'd need to handle.

Here, you should write that message to stderr as it's not your command's normal output (doesn't apply to the ping example though). Then it wouldn't go through the pipe.

Or you could have the subshell running the while loop ignore the SIGINT so it keeps reading the loopHelloWorld output after the SIGINT:

loopHelloWorld | (
  trap '' INT
  while IFS= read -r line; do
    printf '%s\n' "$line"
  done
)

that would however cause the exit status of the pipeline to be 0 when you press Ctrl+C.

Another option for that specific example would be to use zsh or ksh93 instead of bash. In those shells, the while loop would run in the main shell process, so would not be affected by SIGINT.

That wouldn't help for loopHelloWorld | cat though where cat and loopHelloWorld run in the foreground process group.


Use a trap:

#! /bin/bash

trap handleInt SIGINT

interrupted=0

function handleInt {
    echo "Interrupted..."
    interrupted=1
}


while true
do
    [[ interrupted -ne 0 ]] && { echo "Ctrl-C caught, exiting..." ; exit ; }
    echo "Sleeping..."
    sleep 1
    read -r line
    echo "$line"
done

Tags:

Bash

Pipe