Control flow in Bash: if, while and traps

Objectives

  1. Be able to perform simple tests and influence execution

  2. Keep control even when your script is sent a signal

To follow along

Please navigate to the example/control_flow directory.

cd ../../examples/control_flow

Control flow statements

The most important control flow statements in Bash are

  • if/else blocks

  • while loops

They are used in conjunction with anything that has a return status.

In particular:

  • the test command, equivalent to [ ], or with the command [[ ]] (a Bash extension).

  • Arithmetic comparisons using (( <condition> ))

But they can be used with any process that terminates. A return status equal to 0 means “True”, and all other values mean “False”, a ! negates the value of an expression.

Conditional expressions

Search the Bash manual for the section on CONDITIONAL EXPRESSIONS.

  1. What expression can be used to test if a file or directory already exists?

  2. How to compare two numbers?

Tip

Control expressions have exit statuses

Note: Spaces around the square brackets are important!

if-else statements

We can check if a file exists, and if not, we can create it:

if ! [ -f myfile.txt ]
then
   touch myfile.txt
   echo "file created now"
else
   echo "file exists already"
fi
file exists already

&&, || and !

&&, || and ! are boolean logic operators that can be used to combine commands in a list, while at the same time combining their return codes.

Interestingly:

  • A list of commands joined by && will run only until one of them fails.

  • A list of commands joined by || will run only until one of them succeeds.

If the body of the if statements are short, && and || can be used more conveniently to simulate an if statement (but perhaps in a less readable way):

! [ -f myfile.txt ] && { touch myfile.txt; echo "file created now"; } || echo "file exists already"
file exists already

In this case, we have collected some steps together in braces {}.

Be precise!

Actually,

condition && then_action || else_action

is not a precise translation of

if condition
then then_action
else else_action
fi

Can you figure out the case where the two expressions behave differently? How can you fix the first expression so that it behaves more precisely like the second one?

While loops

The syntax is similar to for loops, but the header is different:

while ! [ -f myfile.txt ]
do
   echo "File does not yet exist"
   # waiting a little before repeating the loop
   sleep 5
done

Until loops

The loop above could be rewritten more conveniently using a until loop. What should be changed?

Dealing with signals: traps

When your script receives a signal that it does know how to handle, by default it terminates.

With the trap command, it is possible to catch and handle some of the signals, typically before aborting, for example cleaning up.

Note

This example is contained in a separate script because it is better to run it in a separate subshell, and it is easier if this is done by running the script.

cat trap-example.sh 
cleanup(){
  echo "Cleanup before interrupt and exit"
  exit 0
}

(
trap "cleanup" SIGINT

while true
do
  sleep ${SLEEP_INTERVAL:-1}
done
) & 

sleep 5
kill -SIGINT %1 # Sending the signal to the process in the background.
echo "Sent SIGINT at $SECONDS seconds"
wait
echo "Subprocess terminated at $SECONDS seconds"

When is a trap executed?

Notice that the function set with the trap command can be executed only when the bash shell has control of the execution, i.e. between commands invocations in the shell.

The default behaviour of this script is the following:

unset SLEEP_INTERVAL
bash ./trap-example.sh
Sent SIGINT at 5 seconds
Cleanup before interrupt and exit
Subprocess terminated at 5 seconds

Thanks to the fact that the shell is waking up every second, the signal handler defined by trap can be invoked timely.

But if we set SLEEP_INTERVAL to, for example, 10 seconds, then the signal handler will not be invoked when the signal is sent, but only later, at the first occasion when the shell takes control:

export SLEEP_INTERVAL=10
bash ./trap-example.sh
Sent SIGINT at 5 seconds
Cleanup before interrupt and exit
Subprocess terminated at 10 seconds

Note

The trap mechanism, together with the --signal option for sbatch, can be used to trigger a controlled termination of a slurm job. See this example