Shell scripting | Control structures & flow control
Index
- Control structures
- Flow control: conditional execution
- Flow control: conditional if
- Flow control: conditional case
- Flow control: for loop
- Flow control: while loop
Sometimes we want to store more than a single value in a variable. And sometimes decisions have to be made for a hundred times. Let's jump into flow control with loops and the use of control structures.
Control structures and flow control allow us to make decisions on our code based on the processed data.
When making scripts to handle repetitive tasks on a system, or helping out in the daily usage of it, there are several things to take care about. One of those things which is critical is how to handle conditions and redirect the flow of the script.
Control structures
Just having the ability to create and store variables doesn't give us too much power. Comparing and testing data is an essential part in programming.
In order to compare and evaluate our variables' data we have a series of operators:
File operators
Operators are placed before evaluating the variable:
-N "$file"
where N
is the desired operator to evaluate.
-e
returns true if the file or directory exist.-d
returns true if the directory exists.-f
returns true if the file exists.-s
returns true if the file exists and it's not empty.
We can also check if the files or directories have read, write and executable (files only) permissions.
-r
returns true if read permission is granted.-w
returns true if write permission is granted.-x
returns true if executable permission is granted.
String operators
-z
returns true if string is zero-length.
-z "$string"
-n
returns true if string's length is non-zero.
-n "$string"
=
returns true ifstring a
is equal tostring b
. This returns inconsistent values when comparing integers.
"$string_a" = "$string_b"
!=
returns true ifstring A
is not equal tostring B
.
"$string_a" != "$string_b"
Integer operators
Integer operators are used to compare integers and are placed between to variables to evaluate:
"$int_a -NN "$int_b"
where NN
is the desired operator to use.
-eq
returns true if both integers are equal.-ne
returns true if integers are not equal.-gt
returns true if integer A is greater than integer B.-ge
returns true if integer A is greater than or equal to integer B.-lt
returns true if integer A is less than integer B.-le
returns true if integer A is less than or equal to integer B.
Flow control: conditional execution
Conditional executions work based on the exit status of other command. Their main advantage is allowing scripts and functions to run in “short circuit” or exit early. They are a bit faster than an if structure.
Conditional execution operators are &&
and ||
. These operators have no precedence and they are left-associative.
- AND
&&
operator will run only if the first action was successful.
$ cd .scripts/ && pwd
- OR
||
operator will run only if the first action wasn't successful.
$ cd .garbage/ || exit
rm -rf *
A third operator named logical not is useful in the game.
- NOT
!
operator is used to test whether expression is true or not.
$ test ! -f .scripts/demo.sh && echo "File not found."
It's possible to combine multiple statements, always remembering the left-associative property.
Flow control: conditional if
Conditional structures like if
allow us to perform different actions for different decisions.
A default if
statement looks like this:
if [[ $1 -eq $user ]]; then
printf "%s\n" "$user you're logged in"
fi
However optional clauses elif
and/or else
can be added:
if [[ $1 -eq $user ]]; then
printf "%s\n" "$user you're logged in"
elif [[ $1 -gt $max ]]; then
printf "%s\n" "Your user name has to be less than $max characters"
else
printf "%s\n" "You must type your username."
fi
Also nested if statements are allowed:
if [ condition ]; then
if [ condition ]; then
#action
else
#action
fi
else
#action
fi
The content inside the brackets [[
is treated as a command and it's the exit code of that command what is tested, thus the brackets are not part of the if syntax.
The exit code is true
if it exits with 0
, and false
if it exits with 1
.
This way we can also use conditional operators in an if statement:
if [ -r $1 ] && [ -s $1 ]; then
cat $1
fi
Mathematical expressions return 0
or 1
when placed between double parenthesis.
if (( $1 + $2 > 10 )); then
printf "%s\n" "Those are too many apples."
fi
Flow control: conditional case
Case statements provide a good alternative to multilevel if statements when you have to match multiple values against one variable.
— Case statements execute the case inside the structure that matches the given pattern.
read -p "please, enter a number to select: " pattern
case $pattern in
1)
printf "%s\n" "First choice. Nice one"
;;
2)
printf "%s\n" "There we go."
;;
3)
printf "%s\n" "Three is always a good choice."
;;
*)
printf "%s\n" "We're sorry, choose between 1-3."
;;
esac
— Case statements are enclosed between the word case and the word esac
. The operator ;;
breaks after the first match, if any.
As you may notice we have a case that is an asterisk *
. It represents any value and behaves similar to a default case. We can cover ourselves in a situation where the given pattern doesn't match any given cases, the catch-all one is executed and we don't make our program to exit with errors.
— We can also make our case statement to work using multiple patterns:
read -p "please, enter a vehicle to inspect: " vehicle
case $vehicle in
car|truck|van)
printf "%s\n" "Ground vehicle. Has tires and a combustion engine."
;;
boat|submarine)
printf "%s\n" "Water vehicle. Not functional in a desert."
;;
plane|helicopter)
printf "%s\n" "It can fly! It serves multiple purposes."
;;
*)
printf "%s\n" "We're sorry, your vehicle doesn't exist."
;;
esac
Flow control: for loop
This way of flow control works iterating trough values in a list until the end is reached. for
loops perform a set of commands for each item in the list.
A simple for loop structure looks like this:
for var in values; do
#commands to execute $var
done
As an example, let's imagine we have a directory with a bunch of files, and we want to list only which of them are .png
images.
my_directory=/path/to/my_directory/
images=*.png
for i in "$my_directory""$images"; do
printf "%s\n" "$i"
done
— The shell also understands C-Style for loops, which have this structure:
for (var=1; var < n; var++); do
#commands to execute $var
done
— We can perform some control inside a for loop using the continue and break commands.
continue
tells the Shell to skip the current loop and jump into the nextvalue
invalues
.
for val in values; do
#command a
#command b
if(condition to jump over c); then
continue
fi
#command c
done
break
tells the Shell to leave the for loop straight away.
for val in values; do
#command a
#command b
if(condition to break the loop); then
command c
break
fi
#command d
done
— As in other programming languages, we can perform nested for
loops, this is, a loop within a loop. They are handy when we want to repeat more than one action several times. They can be independent or not.
for (i=1; i < n; i++); do
#commands to execute $i (if any before entering the next loop)
for (j=1; j < i; j++); do
#commands to execute $i $j
done
done
Flow control: while loop
As their name indicate, while an expression is true, the loop will run the inner lines of code.
A simple while loop structure looks like this:
while [ test ]; do
#commands to execute
done
while
loops evaluate the exit status to check if they have to stop or not. A while loop will run for as long as the exit status evaluated inside [[]]
equals to zero.
num=0
while [[ "$num" -lt 5 ]]; do
printf "%s\n" "$num"
num=$((num + 1))
done
printf "%s\n" "We've reached the limit."
— The same way we are able to control for loops with continue and break, we can control while loops too.
num=0
while [[ "$num" -lt 5 ]]; do
if [[ "$num" == 3 ]]; then
printf "%s\n" "exited because num is equal to 3."
break
done
num=$((num + 1))
done
— while loops can be controlled by user input too taking the advantage of infinite loops.
Infinite loops are defined by adding : after the word while.
while :
do
#commands inside infinite loop
done
To stop an infinite loop, press the CTRL+C
key combination, or include a value to be understood as a loop end:
while :
do
read -p "type EXIT to end program: " end
printf "%s\n" "You typed $end"
if [ "$end" == "EXIT" ]; then
exit 0
done
done