Shell scripting | Control structures & flow control

FreeBSD gearing-up

Index


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:

where N is the desired operator to evaluate.

We can also check if the files or directories have read, write and executable (files only) permissions.

String operators

-z "$string"
-n "$string"
"$string_a" = "$string_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.

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.

$ cd .scripts/ && pwd
$ cd .garbage/ || exit
rm -rf *

A third operator named logical not is useful in the game.

$ 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.

for val in values; do
    #command a 
    #command b
    if(condition to jump over c); then
        continue
    fi
    #command c
done
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