1. Introduction

The Linux system uses signals, which we can regard as events triggered under specific conditions. In Bash scripts, we can listen to this signal to respond correctly to these circumstances.

In this tutorial, we’ll learn to handle the common SIGINT signal emitted when the user presses the Ctrl+C combination.

2. The trap Command

The trap built-in allows the execution of Bash commands when the signal arrives. Therefore, we can use it to customize the signal handling or ignore it altogether. We’re going to use the syntax:

trap [-lp] [action] [signal]

action is a string with Bash commands, which can be parsed and evaluated in the way that eval does. signal is a signal’s value or name. trap recognizes the signal’s short name, e.g., INT for SIGINT. Depending on the implementation, we can also use full names.

Finally, the -l option lists all signals, while -p prints the action attached to the given signal.

3. Handling SIGINT

Let’s write a simple script that intercepts the SIGINT signal and asks for confirmation:

#!/bin/bash

ctrlc_received=0

function handle_ctrlc()
{
    echo
    if [[ $ctrlc_received == 0 ]]
    then
        echo "I'm hmmm... running. Press Ctrl+C again to stop!"
        ctrlc_received=1
    else
        echo "It's all over!"
        exit
    fi
}

# trapping the SIGINT signal
trap handle_ctrlc SIGINT

while true
do
    echo "I'm sleeping"
    sleep 15
done

Let’s play a bit with this simple program and then summarize the observations:

  • We need to properly handle the signal on our own. So, we use the exit command after the second interruption is detected.
  • SIGINT interrupts the current action.
  • Bash doesn’t resume the interrupted operation. The control is passed to the next command in the script. In this case, it’s the while loop, which in turn starts a new sleep command.

4. Ignoring Signals

As we’ve learned, the interrupted command is not concluded. So, we can get into trouble if the interruption signal comes during some important actions, even if we’ve trapped it. The remedy is to disable signals before running the critical section of the code and enable them later on.

We can use an empty string “” as an action to disable the SIGINT signal. After the critical section is done, we should pass the (minus) action to enable Ctrl+C again:

#!/bin/bash

trap "" SIGINT

#critical section of the code

trap - SIGINT

#less critical part

Note that the action restores the default signal handling. We can’t bring back custom handlers in this way.

It is worth noting that we can’t disable or ignore SIGKILL and SIGTERM.

5. Chaining Signal Handlers

We can trigger multiple actions by one signal with the Bash syntax for chaining commands:

trap 'command1;command2;command3' SIGINT

Let’s assume that our script trap_INT_chain demands a separate handler for each operation, e.g., for log writing and cleanup:

#!/bin/bash

function log_INT()
{
    echo
    echo "Logging interruption" 
} 

function clean_on_INT()
{
    echo
    echo "Cleaning on interruption" 
}

# trapping the SIGINT signal
trap 'log_INT;clean_on_INT;exit' INT

while true
do
    echo "I'm sleeping"
    sleep 15
done

Now, after the script termination, we’re going to obtain the following:

$ ./trap_INT_chain
I'm sleeping
^C
Logging interruption

Cleaning on interruption

6. More Chaining

When we use handlers in a bigger script, we shouldn’t drop the already added handlers. We can achieve it with the help of the -p option to trap, which shows the handler attached to the signal. Let’s check it in the terminal:

$ trap 'echo SIGINT received and ignored!' INT
$ trap -p INT
trap -- 'echo SIGINT received and ignored!' SIGINT

Subsequently, we can get the commands to be executed with cut:

$ trap -p INT | cut -f2 -d \'
echo SIGINT received and ignored!

Now let’s assume that a handler is set for the INT signal. So, we can use the trap -p output to paste the existing commands:

$ trap "$( trap -p INT | cut -f2 -d \' );newCommand" INT

Things get more complicated when we don’t know if any handler is already set. To avoid a leading semicolon, we need to discern the empty trap‘s output:

$ commands="$( trap -p INT | cut -f2 -d \' )"
$ trap "${commands}${commands:+;}newCommand" INT

The work is done by the ${A:+B} operator. It returns B if A is set or a null string if A is unset.

7. Conclusion

In this article, we used the SIGINT signal as an example to learn the trap command. First, we demonstrated the custom Ctrl+C handler. Next, we learned to disable this signal to protect critical parts of the code. Finally, we chained multiple commands and attached them to one signal.