Control flow

Authors

Marie-Hélène Burle

Alex Razoumov

By default, scripts get executed linearly from top to bottom. Often however, you want to control what gets executed when.

This section covers various ways to control the flow of execution through a script.

For this section, we will play with files created by The Carpentries.

You can download them into a zip file called data.zip with:

curl --output data.zip https://mint.westdri.ca/bash/data.zip

You can then unzip that file with:

unzip data.zip

You should now have a data directory.

cd into it:

cd data

Normal execution of commands

Commands get executed from top to bottom and from left to right. Different commands are separated by a line break and/or a semi-colon.

Example:

Look at the following commands:

unzip bash.zip
rm bash.zip

This is equivalent to:

unzip bash.zip;
rm bash.zip

and to:

unzip bash.zip; rm bash.zip

This is what we did to get the data for the past few sessions.

In all three cases, both commands will try to run. Now, if for some reason, the unzipping fails, the zip file still gets deleted. That’s a bummer.

Conditional on previous command

Execution conditional on success

Commands can be limited to running only if the previous command ran successfully thanks to the double-ampersand (&&).

Example:

unzip bash.zip &&
    rm bash.zip

This is equivalent to:

unzip bash.zip && rm bash.zip

If the unzipping works (if it returns a zero exit status), then the Zip file gets deleted. If however, the unzipping fails (if it returns a non-zero exit status), the script aborts and we haven’t lost our Zip file.

Execution conditional on failure

The opposite of && is ||: the second command only gets executed if the first one failed.

Example:

unzip bash.zip || echo "Unzipping failed"

This can also be written as:

unzip bash.zip ||
    echo "Unzipping failed"

Conditional executions

Commands can be executed or not depending on some conditions. To achieve this, we first need to have expressions that define these conditions.

Predicates

Predicates are expressions that, when evaluated, return either true or false.

Here are examples of predicates:

[ $var == 'text' ] checks whether var is equal to 'text'.

[ $var == number ] checks whether var is equal to number.

[ -e name ] checks whether name exists.

[ -d name ] checks whether name is a directory.

[ -f name ] checks whether name is a file.

Make sure to have spaces around each bracket.

Your turn:

  • Create a directory d1 and a file f1.
  • Write the predicates that test whether:

  • d1 exists,

  • d1 is a file,

  • d1 is a directory,

  • f1 is a file,

  • f1 is a directory.

If statements

Syntax

In its simplest form, if statements look like:

if [ predicate ]
then
    command1
    command2
    ...
fi

This can also be written as:

if [ predicate ]; then command1; command2; ...; fi

If the condition is true, the commands are executed, if the condition is false, nothing happens.

Examples

var=f1

if [ -e $var ]
then
    echo "$var exists"
fi
f1 exists
var=f2

if [ -e $var ]
then
    echo "$var exists"
fi

Your turn:

Write a conditional expression that prints “d1 is a directory” if d1 is a directory and test it.

If else statements

Syntax

If you want a different set of commands to be executed when the condition is false, you add an else statement:

if [ predicate ]
then
    command1
    command2
    ...
else
    command3
    command4
    ...
fi

Examples

var=f1

if [ -e $var ]
then
    echo "$var exists"
else
    echo "$var does not exist"
fi
f1 exists
var=f2

if [ -e $var ]
then
    echo "$var exists"
else
    echo "$var does not exist"
fi
f2 does not exist

If elif else statements

Of course, you can have multiple conditions defining trees of if statements. In that case, you use elif (any number of times):

Syntax

if [ predicate1 ]
then
    command1
    command2
    ...
elif [ predicate2 ]
then
    command3
    command4
    ...
else
    command5
    command6
    ...
fi

Examples

var=4

if (( $var < 0 ))
then
    echo "$var is negative"
elif (( $var > 0 ))
then
    echo "$var is positive"
else
    echo "$var is equal to zero"
fi
4 is positive

Your turn:

Play with the value of var to test our if elif else statement.

Conditionally repeated executions

Commands can be executed as long as a condition returns True thanks to while loops.

Syntax

The syntax of a while loop in Bash is:

while [ predicate ]
do
    command1
    command2
    ...
done

Example

var=0

while (($var<10))
do
    echo "$var"
    ((var++))
done
0
1
2
3
4
5
6
7
8
9

Be careful that while loops can lead to infinite loops. Such loops need to be manually interrupted (by pressing <Ctrl+C>).

Example of infinite loop:

var=1

while (($var>0))
do
    echo "$var (Press <Ctrl+C> to stop)"
    ((var++))
    sleep 1
done

Executions repeated over a collection

Commands can be repeated for each element of a list thanks to for loops.

Collections

For loops run a set of commands for each item of a collection. How do you create those collections?

Listing items one by one

The least efficient method is to list all the items one by one:

for i in file1 file2 file3
do
    echo $i
done
file1
file2
file3

Wildcards

ls *.pdb
cubane.pdb
ethane.pdb
methane.pdb
octane.pdb
pentane.pdb
propane.pdb

Brace expansion

Collections can also be created with brace expansion.

Examples:

echo {1,2,5}
1 2 5

Make sure not to add a space after the commas.

echo {list,of,strings}
list of strings
echo {file1,file2}.sh
file1.sh file2.sh
ls {ethane,methane,pentane}.pdb
ethane.pdb
methane.pdb
pentane.pdb
echo {1..5}
1 2 3 4 5
echo {01..10}
01 02 03 04 05 06 07 08 09 10
echo {r..v}
r s t u v
echo {v..r}
v u t s r
echo {a..e}{1..3}
a1 a2 a3 b1 b2 b3 c1 c2 c3 d1 d2 d3 e1 e2 e3
echo {a..c}{a..c}
aa ab ac ba bb bc ca cb cc
echo {1..5}.txt
1.txt 2.txt 3.txt 4.txt 5.txt
echo file{3..6}.sh
file3.sh file4.sh file5.sh file6.sh

Sequences

Collections can also be sequences:

seq 1 2 10
1
3
5
7
9

Here, 1 is the start of the sequence, 10 is the end, and 2 is the step.

For loops

Syntax

The general structure of a for loop is as follows:

for iterable in collection
do
    command1
    command2
    ...
done

Examples

The molecules directory contains a number of .pdb files. We want to rename them by prepending “gas_” to their current names.

We can do this by creating a collection with a wildcard and applying the command to each element of the collection with a for loop:

for file in *.pdb
do
    mv $file gas_$file
done

This can also be written as:

for file in *.pdb; do mv $file gas_$file; done

Here is a for loop using a collection created by a sequence:

for i in $(seq 1 2 10)
do
    echo file$i.txt
done
file1.txt
file3.txt
file5.txt
file7.txt
file9.txt

Your turn:

In a directory the command ls returns:

fructose.dat  glucose.dat  sucrose.dat  maltose.txt

What would be the output of the following loop?

for datafile in *.dat
do
  cat $datafile >> sugar.dat
done
  1. All of the text from fructose.dat, glucose.dat and sucrose.dat would be concatenated and saved to a file called sugar.dat.

  2. The text from sucrose.dat will be saved to a file called sugar.dat.

  3. All of the text from fructose.dat, glucose.dat, sucrose.dat, and maltose.txt would be concatenated and saved to a file called sugar.dat.

  4. All of the text from fructose.dat, glucose.dat and sucrose.dat will be printed to the screen and saved into a file called sugar.dat.

Here is a video of a previous version of this workshop.