Control Structures

Learn decision-making and looping in Bash with conditionals and loops to create dynamic scripts

Control structures are essential components of any programming language, allowing you to control the flow of execution in your scripts. In this section, we'll explore Bash's conditional statements and loops that enable you to make decisions and repeat actions.

Conditional Statements

Conditional statements let your script make decisions based on certain conditions.

if-else Statements

The basic syntax of an if statement in Bash:

if [ condition ]; then
    # Commands to execute if condition is true
fi

Adding an else clause:

if [ condition ]; then
    # Commands for true condition
else
    # Commands for false condition
fi

For multiple conditions, use elif (else if):

if [ condition1 ]; then
    # Commands for condition1
elif [ condition2 ]; then
    # Commands for condition2
else
    # Commands for all other cases
fi

Here's a practical example that checks if a file exists:

#!/bin/bash

filename="config.txt"

if [ -f "$filename" ]; then
    echo "$filename exists."
else
    echo "$filename does not exist."
    echo "Creating $filename..."
    touch "$filename"
fi

Test Conditions

The square brackets [ ] in the examples above are actually a shorthand for the test command. Here are some common test conditions:

File Tests:

[ -f file ]  # True if file exists and is a regular file
[ -d dir ]   # True if dir exists and is a directory
[ -e file ]  # True if file exists (any type)
[ -r file ]  # True if file exists and is readable
[ -w file ]  # True if file exists and is writable
[ -x file ]  # True if file exists and is executable
[ -s file ]  # True if file exists and has size greater than zero

String Tests:

[ -z string ]      # True if string is empty
[ -n string ]      # True if string is not empty
[ string1 = string2 ]  # True if strings are equal
[ string1 != string2 ] # True if strings are not equal

Integer Comparisons:

[ int1 -eq int2 ]  # Equal to
[ int1 -ne int2 ]  # Not equal to
[ int1 -lt int2 ]  # Less than
[ int1 -le int2 ]  # Less than or equal to
[ int1 -gt int2 ]  # Greater than
[ int1 -ge int2 ]  # Greater than or equal to

Logical Operators:

[ condition1 -a condition2 ]  # AND (both must be true)
[ condition1 -o condition2 ]  # OR (at least one must be true)
[ ! condition ]               # NOT (negation)

Modern Test Syntax

Bash also provides the [[ ]] construct, which is more powerful than the traditional [ ]:

if [[ condition ]]; then
    # Commands
fi

Advantages of [[ ]] include:

  • No need to quote variables to handle spaces
  • Support for additional operators like && and || for logical AND and OR
  • Pattern matching with the =~ operator for regular expressions

Example using [[ ]]:

#!/bin/bash

filename="my document.txt"

# No need to quote $filename with [[ ]]
if [[ -f $filename && -r $filename ]]; then
    echo "$filename exists and is readable."
fi

# Regular expression matching
email="[email protected]"
if [[ $email =~ [a-z]+@[a-z]+\.[a-z]+ ]]; then
    echo "$email is a valid email format."
fi

Arithmetic Tests with (( ))

For numeric comparisons, you can use the (( )) construct, which allows for more natural mathematical syntax:

if (( expression )); then
    # Commands
fi

Example:

#!/bin/bash

age=25

if (( age >= 18 )); then
    echo "Adult"
else
    echo "Minor"
fi

# You can use variables without $
if (( age * 2 > 40 )); then
    echo "Double your age is greater than 40"
fi

case Statement

The case statement provides a cleaner way to match multiple values:

case $variable in
    pattern1)
        # Commands for pattern1
        ;;
    pattern2)
        # Commands for pattern2
        ;;
    *)
        # Default case (like else)
        ;;
esac

Example:

#!/bin/bash

fruit="apple"

case $fruit in
    "apple")
        echo "It's an apple."
        ;;
    "banana" | "plantain")
        echo "It's a banana or plantain."
        ;;
    "orange" | "tangerine")
        echo "It's a citrus fruit."
        ;;
    *)
        echo "Unknown fruit."
        ;;
esac

Loops

Loops allow you to repeat commands multiple times.

for Loops

The basic syntax for a for loop:

for variable in list; do
    # Commands using $variable
done

Examples:

# Loop through a list of strings
for fruit in apple banana orange; do
    echo "I like $fruit"
done

# Loop through an array
fruits=("apple" "banana" "orange")
for fruit in "${fruits[@]}"; do
    echo "Processing $fruit"
done

# Loop through a range of numbers
for i in {1..5}; do
    echo "Number: $i"
done

# Loop with step value (Bash 4.0+)
for i in {1..10..2}; do  # Increment by 2
    echo "Odd number: $i"
done

# C-style for loop
for ((i=0; i<5; i++)); do
    echo "Count: $i"
done

while Loops

The while loop executes as long as a condition is true:

while [ condition ]; do
    # Commands
done

Examples:

# Simple counter
count=1
while [ $count -le 5 ]; do
    echo "Count: $count"
    ((count++))
done

# Reading lines from a file
while read line; do
    echo "Line: $line"
done < input.txt

# Infinite loop (requires explicit break)
while true; do
    echo "Press q to quit"
    read -n 1 input
    if [ "$input" = "q" ]; then
        break
    fi
done

until Loops

The until loop is similar to while, but runs until a condition becomes true:

until [ condition ]; do
    # Commands
done

Example:

count=1
until [ $count -gt 5 ]; do
    echo "Count: $count"
    ((count++))
done

Loop Control

Bash provides statements to control loop execution:

# Skip the current iteration
for i in {1..10}; do
    if (( i % 2 == 0 )); then
        continue  # Skip even numbers
    fi
    echo "Odd number: $i"
done

# Exit the loop entirely
for i in {1..100}; do
    echo "Number: $i"
    if [ $i -eq 5 ]; then
        break  # Stop at 5
    fi
done

Combining Control Structures

You can nest control structures to create more complex logic:

#!/bin/bash

# Check all files in current directory
for file in *; do
    if [ -f "$file" ]; then
        echo "$file is a regular file"

        if [ -x "$file" ]; then
            echo "  and it's executable"
        elif [ -r "$file" ]; then
            echo "  and it's readable"
        fi

        # Get file extension
        filename=$(basename "$file")
        extension="${filename##*.}"

        case $extension in
            "txt")
                echo "  Text file detected"
                ;;
            "sh")
                echo "  Shell script detected"
                ;;
            *)
                echo "  Unknown file type"
                ;;
        esac
    elif [ -d "$file" ]; then
        echo "$file is a directory"
    fi
done

Error Handling

Use conditional statements to handle errors:

#!/bin/bash

# Attempt to create a directory
mkdir -p /tmp/test_dir

# Check the exit status
if [ $? -eq 0 ]; then
    echo "Directory created successfully"
else
    echo "Failed to create directory"
    exit 1  # Exit with error status
fi

You can also use the || operator for error handling:

# If mkdir fails, execute the second command
mkdir /tmp/test_dir || echo "Failed to create directory"

# More complex handling
mkdir /tmp/test_dir || { echo "Failed to create directory"; exit 1; }

Similarly, use && to execute a command only if the previous one succeeded:

# Change to the directory only if it was created successfully
mkdir /tmp/test_dir && cd /tmp/test_dir

Practical Examples

Let's put everything together with some real-world examples:

Processing Files in a Directory

#!/bin/bash

# Process all text files in the current directory
for file in *.txt; do
    # Skip if no txt files exist
    if [ "$file" = "*.txt" ]; then
        echo "No text files found."
        break
    fi

    echo "Processing $file..."

    # Skip empty files
    if [ ! -s "$file" ]; then
        echo "  Skipping empty file"
        continue
    fi

    # Count lines, words, and characters
    lines=$(wc -l < "$file")
    words=$(wc -w < "$file")
    chars=$(wc -c < "$file")

    echo "  Lines: $lines"
    echo "  Words: $words"
    echo "  Characters: $chars"
done

Interactive Menu

#!/bin/bash

while true; do
    clear
    echo "--------------------------"
    echo "       System Menu       "
    echo "--------------------------"
    echo "1. Show date and time"
    echo "2. Show system uptime"
    echo "3. Show logged-in users"
    echo "4. Check disk space"
    echo "5. Exit"
    echo
    read -p "Enter your choice [1-5]: " choice

    case $choice in
        1)
            echo
            date
            ;;
        2)
            echo
            uptime
            ;;
        3)
            echo
            who
            ;;
        4)
            echo
            df -h
            ;;
        5)
            echo "Exiting..."
            exit 0
            ;;
        *)
            echo "Invalid option. Please try again."
            ;;
    esac

    echo
    read -p "Press Enter to continue..."
done

With these control structures, you can create dynamic, responsive scripts that make decisions and handle different situations effectively. In the next section, we'll explore functions, which allow you to organize your code into reusable blocks.

Found an issue?