Skip to main content

3. Traps. Asynchronous execution. Named pipes.

  1. We know that programs can respond to signals. We can add this capability to our scripts too. Bash provides a mechanism for this purpose known as a trap. Let's see a simple example:

    vim trap-demo.sh
    trap-demo.sh
    #!/bin/bash

    # trap-demo: simple signal handling demo

    trap "echo 'I am ignoring you.'" SIGINT SIGTERM

    for i in {1..5}; do
    echo "Iteration $i of 5"
    sleep 5
    done

    When we press Ctrl-c while the script is running, the script will intercept the signal and will respond to it by running the echo command. Let's try it:

    ./trap-demo.sh

    Press Ctrl-c a few times and see what happens.

  2. It is more convenient to tell trap to call a function in response to a signal, instead of a complex command. Let's see another example:

    vim trap-demo2.sh
    trap-demo2.sh
    #!/bin/bash

    # trap-demo2: simple signal handling demo

    exit_on_signal_SIGINT () {
    echo "Script interrupted." 2>&1
    exit 0
    }

    exit_on_signal_SIGTERM () {
    echo "Script terminated." 2>&1
    exit 0
    }

    trap exit_on_signal_SIGINT SIGINT
    trap exit_on_signal_SIGTERM SIGTERM

    for i in {1..5}; do
    echo "Iteration $i of 5"
    sleep 5
    done

    Note the inclusion of an exit command in each of the signal-handling functions. Without an exit, the script would continue after completing the function.

    ./trap-demo2.sh

    Press Ctrl-c.

  3. Bash has a builtin command to help manage asynchronous execution. The wait command causes a parent script to pause until a specified process (i.e., the child script) finishes.

    This can be best explained by an example. We will need two scripts, a parent script, and a child script:

    vim async-child.sh
    async-child.sh
    #!/bin/bash

    # async-child: Asynchronous execution demo (child)

    echo "Child: child is running..."
    sleep 5
    echo "Child: child is done. Exiting."

    This is a simple script that runs for 5 seconds.

    vim async-parent.sh
    async-parent.sh
    #!/bin/bash

    # async-parent: Asynchronous execution demo (parent)

    echo "Parent: starting..."

    echo "Parent: launching child script..."
    ./async-child.sh &
    pid=$!
    echo "Parent: child (PID= $pid) launched."

    echo "Parent: continuing..."
    sleep 2

    echo "Parent: pausing to wait for child to finish..."
    wait "$pid"

    echo "Parent: child is finished. Continuing..."
    echo "Parent: parent is done. Exiting."

    From this script we launch the child script. Since we are appending & after it, the parent script will not wait for the child to finish executing but will continue running. Both of the scripts are now running in parallel. Immediately after launching the child, the parent uses the special variable $! to get the process ID (PID) of the child. This variable always contains the PID of the last job put into the background. Then, later in the parent script, we use the command wait to stop the parent from running any further, until the child script is finished. Let's try it:

    ./async-parent.sh

    All the messages output from the parent are prefixed with Parent: and all the messages output from the child are prefixed with Child:. This helps us understand the flow of execution.

  4. Named pipes behave like files but actually form first-in first-out (FIFO) buffers. As with ordinary (unnamed) pipes, data goes in one end and emerges out the other.

    With named pipes, it is possible to set up something like this: process1 > named_pipe, an this: process2 < named_pipe and it will behave like this: process1 | process2. The only difference is that process1 and process2 run in the current shell, not in a subshell, which makes named pipes more useful, even if they are a bit less convenient than using a pipe operator (|).

    A named pipe can be created with the command mkfifo:

    mkfifo pipe1
    ls -l pipe1

    Notice that the first letter in the attributes field is "p", indicating that it is a named pipe.

    ls -l > pipe1 &
    cat < pipe1

    This is similar to:

    ls -l | cat

    However the named pipe is more flexible, because the two commands connected by the pipe can be executed even in different terminals.

    However these two examples are not the same thing:

    echo "abc" | read
    echo $REPLY
    echo "abc" > pipe1 &
    read < pipe1
    echo $REPLY

    Let's remove pipe1:

    rm pipe1
  5. Another example with a named pipe:

    mkfifo pipe1
    while true; do read line < pipe1; echo "You said: '$line'"; done &
    echo Hi > pipe1
    echo Hello > pipe1
    echo "The quick brown fox jumped over the lazy dog." > pipe1
    fg

    Press Ctrl-c to stop the while loop.

    rm pipe1
Loading asciinema cast...