Control flow in Bash: if, while and traps
Objectives
Be able to perform simple tests and influence execution
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/elseblockswhileloops
They are used in conjunction with anything that has a return status.
In particular:
the
testcommand, 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.
What expression can be used to test if a file or directory already exists?
How to compare two numbers?
Solution
[ -a a/path/ ]will check ifa/path/exists[ $N1 -lt $N2 ]will check if $N1 is less than $N2. There are 5 other related checks.ARIHTMETIC EVALUATIONcan also be used in this case:(( N1 < N2 ))
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?
Solution
When the condition evaluates to true, but the then_action evaluates to false, then the else_action will be executed anyway.
This more complex expression behaves more precisely like an if statement:
condition && { then_action || true ; } || else_action
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?
Solution
until [ -f myfile.txt ]
do
echo "File does not yet exist"
# waiting a little
sleep 2
done
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