funwithlinux guide

Exploring Conditional Statements in Bash Scripting

Bash scripting is a powerful tool for automating tasks in Unix-like systems, from simple file management to complex system administration. At the heart of any useful script lies the ability to make decisions—and that’s where **conditional statements** come into play. Conditional statements allow your script to execute different code blocks based on whether a condition is true or false, enabling dynamic and responsive behavior. Whether you’re checking if a file exists, validating user input, or comparing numbers, conditional statements are indispensable. In this guide, we’ll dive deep into Bash’s conditional logic, covering syntax, operators, practical examples, and best practices to help you master decision-making in your scripts.

Table of Contents

  1. What Are Conditional Statements?
  2. Types of Conditional Statements in Bash
  3. Conditional Expressions and Operators
  4. The case Statement
  5. Practical Examples
  6. Best Practices
  7. Conclusion
  8. References

What Are Conditional Statements?

Conditional statements are control structures that let a script “make choices” by evaluating a condition and executing code based on whether the condition is true (exit status 0) or false (exit status non-zero).

In Bash, conditions are typically checked using test commands (e.g., [ ], [[ ]], or test), which return an exit status indicating the result of the check. For example, [ -f "file.txt" ] returns 0 (true) if file.txt is a regular file, and 1 (false) otherwise.

Types of Conditional Statements in Bash

The if Statement

The simplest conditional is the if statement, which executes a block of code only if a condition is true.

Syntax:

if condition; then
    # Code to run if condition is true
fi
  • condition: A test command (e.g., [ -f "file.txt" ]).
  • then: Separates the condition from the code block (can also be on the same line as if with a semicolon: if condition; then).
  • fi: Closes the if block (reverse of if).

Example: Check if a file exists:

file="example.txt"
if [ -f "$file" ]; then
    echo "$file exists and is a regular file."
fi

The if-else Statement

Use if-else to execute one block when the condition is true and another when it’s false.

Syntax:

if condition; then
    # Code if true
else
    # Code if false
fi

Example: Check if a file exists; create it if not:

file="example.txt"
if [ -f "$file" ]; then
    echo "$file exists."
else
    echo "$file does not exist. Creating it..."
    touch "$file"
fi

The if-elif-else Statement

For multiple conditions, use if-elif-else (“elif” = “else if”). It checks conditions in order and executes the first true block.

Syntax:

if condition1; then
    # Code if condition1 is true
elif condition2; then
    # Code if condition2 is true (and condition1 is false)
else
    # Code if all conditions are false
fi

Example: Categorize a number:

read -p "Enter a number: " num

if [ "$num" -lt 10 ]; then
    echo "Number is less than 10."
elif [ "$num" -lt 20 ]; then
    echo "Number is between 10 and 19."
else
    echo "Number is 20 or greater."
fi

Nested if Statements

You can nest if statements inside other if, elif, or else blocks to check multiple layers of conditions.

Syntax:

if outer_condition; then
    # Outer condition is true
    if inner_condition; then
        # Inner condition is true
    else
        # Inner condition is false
    fi
else
    # Outer condition is false
fi

Example: Check if the user is root, then check if a directory exists:

if [ "$(id -u)" -eq 0 ]; then  # Check if user is root (UID 0)
    echo "Running as root."
    dir="/var/log/myscript"
    if [ -d "$dir" ]; then  # Nested: check if directory exists
        echo "Directory $dir exists."
    else
        echo "Creating $dir..."
        mkdir -p "$dir"
    fi
else
    echo "Error: This script requires root privileges."
    exit 1  # Exit with error code 1
fi

Conditional Expressions and Operators

To evaluate conditions, Bash relies on test commands and operators. Let’s break down the most common tools and how to use them.

Test Command: [ ] vs [[ ]]

Bash provides two primary ways to write conditions:

1. POSIX [ ] (Square Brackets)

The [ ] syntax is a synonym for the test command (e.g., [ -f "file" ] is equivalent to test -f "file"). It is POSIX-compliant, making scripts portable across shells.

Rules for [ ]:

  • Always leave spaces between brackets and the condition (e.g., [ -f file ] works; [-f file] does not).
  • Use quotes around variables to avoid word-splitting (e.g., [ "$var" = "hello" ]).
  • Escape special characters (e.g., [ "$var" = "a*" ] matches literal a*, not a pattern).

2. Bash-Specific [[ ]] (Double Brackets)

[[ ]] is a Bash extension with more features than [ ], including pattern matching, logical operators inside the brackets, and no need to escape special characters. It is not POSIX-compliant, so use it only in Bash scripts.

Advantages of [[ ]]:

  • Supports pattern matching with * and ? (e.g., [[ "$var" == a* ]] checks if var starts with “a”).
  • Logical operators && (and) and || (or) work inside the brackets (no need for -a or -o).
  • No word-splitting issues (variables don’t always need quotes, but quoting is still good practice).

Example: Pattern Matching with [[ ]]

name="Alice"
if [[ "$name" == A* ]]; then  # Checks if name starts with "A"
    echo "Name starts with 'A'."
fi

File Operators

File operators check properties of files/directories. Use them with [ ] or [[ ]].

OperatorDescriptionExample
-eFile/directory exists[ -e "file.txt" ]
-fRegular file (not a directory/symlink)[[ -f "data.csv" ]]
-dDirectory[ -d "/tmp" ]
-rReadable by the current user[[ -r "secret.txt" ]]
-wWritable by the current user[ -w "output.log" ]
-xExecutable by the current user[[ -x "./script.sh" ]]
-sFile is not empty (size > 0)[ -s "log.txt" ]
-LSymbolic link[[ -L "link.txt" ]]

String Operators

String operators compare text values or check string properties.

OperatorDescriptionExample (with [ ])Example (with [[ ]])
=/==Strings are equal (use = in [ ])[ "$str1" = "$str2" ][[ "$str1" == "$str2" ]]
!=Strings are not equal[ "$str1" != "$str2" ][[ "$str1" != "$str2" ]]
-zString is empty (length 0)[ -z "$empty_str" ][[ -z "$empty_str" ]]
-nString is non-empty (length > 0)[ -n "$non_empty_str" ][[ -n "$non_empty_str" ]]
== *pattern*String matches a pattern (Bash-only)N/A (use case instead)[[ "$str" == "he*" ]] (starts with “he”)

Numeric Operators

Numeric operators compare integers. Use these with [ ] or [[ ]].

OperatorDescriptionExample (with [ ])Example (with [[ ]])
-eqEqual to[ "$num" -eq 10 ][[ "$num" -eq 10 ]] or [[ "$num" == 10 ]]
-neNot equal to[ "$num" -ne 5 ][[ "$num" -ne 5 ]] or [[ "$num" != 5 ]]
-gtGreater than[ "$num" -gt 20 ][[ "$num" -gt 20 ]] or [[ "$num" > 20 ]]
-ltLess than[ "$num" -lt 15 ][[ "$num" -lt 15 ]] or [[ "$num" < 15 ]]
-geGreater than or equal to[ "$num" -ge 30 ][[ "$num" -ge 30 ]] or [[ "$num" >= 30 ]]
-leLess than or equal to[ "$num" -le 50 ][[ "$num" -le 50 ]] or [[ "$num" <= 50 ]]

Logical Operators

Combine multiple conditions with logical operators.

Goal[ ] Syntax (POSIX)[[ ]] Syntax (Bash)
AND[ condition1 ] && [ condition2 ][[ condition1 && condition2 ]]
OR`[ condition1 ]
NOT[ ! condition ][[ ! condition ]]

Example: AND Condition

# Check if file exists AND is readable (using [ ])
if [ -f "file.txt" ] && [ -r "file.txt" ]; then
    echo "file.txt exists and is readable."
fi

# Same with [[ ]] (more concise)
if [[ -f "file.txt" && -r "file.txt" ]]; then
    echo "file.txt exists and is readable."
fi

The case Statement

For checking a variable against multiple patterns, the case statement is cleaner than nested if-elif blocks. It uses pattern matching with * (wildcard), ? (single character), and [ ] (character ranges).

Syntax:

case "$variable" in
    pattern1)
        # Code if variable matches pattern1
        ;;
    pattern2 | pattern3)  # Match pattern2 OR pattern3
        # Code if variable matches pattern2 or pattern3
        ;;
    *)  # Default case (matches anything else)
        # Code if no patterns match
        ;;
esac

Example: Simple Menu

echo "Choose an option:"
echo "1. Say Hello"
echo "2. Say Goodbye"
read -p "Enter 1 or 2: " choice

case "$choice" in
    1)
        echo "Hello!"
        ;;
    2)
        echo "Goodbye!"
        ;;
    *)  # Default: invalid input
        echo "Error: Invalid option."
        exit 1
        ;;
esac

Practical Examples

Example 1: Check File Existence and Permissions

This script verifies if a file exists, is readable, and is not empty before processing it.

#!/bin/bash
file="data.txt"

# Check if file exists
if [ ! -e "$file" ]; then
    echo "Error: $file does not exist."
    exit 1
fi

# Check if file is readable
if [ ! -r "$file" ]; then
    echo "Error: $file is not readable."
    exit 1
fi

# Check if file is not empty
if [ -s "$file" ]; then
    echo "Processing $file..."
    # Add your processing logic here (e.g., cat, grep)
else
    echo "Warning: $file is empty. No processing needed."
fi

Example 2: Validate User Input

This script prompts the user for a number and ensures the input is a positive integer.

#!/bin/bash
read -p "Enter a positive integer: " num

# Check if input is empty
if [ -z "$num" ]; then
    echo "Error: Input cannot be empty."
    exit 1
fi

# Check if input is a valid integer (using regex in [[ ]])
if [[ ! "$num" =~ ^[0-9]+$ ]]; then
    echo "Error: '$num' is not a positive integer."
    exit 1
fi

echo "You entered: $num (valid positive integer)."

Example 3: Menu-Driven Script with case

A script to manage a to-do list with add, view, and delete options.

#!/bin/bash
todo_file="todo.txt"

while true; do  # Loop until user chooses exit
    echo -e "\nTo-Do List Manager"
    echo "1. Add Task"
    echo "2. View Tasks"
    echo "3. Delete All Tasks"
    echo "4. Exit"
    read -p "Choose an option (1-4): " choice

    case "$choice" in
        1)
            read -p "Enter task: " task
            echo "$task" >> "$todo_file"
            echo "Task added."
            ;;
        2)
            if [ -s "$todo_file" ]; then
                echo -e "\nYour Tasks:"
                cat -n "$todo_file"  # Show line numbers
            else
                echo "No tasks yet!"
            fi
            ;;
        3)
            rm -f "$todo_file"  # Delete file (if exists)
            echo "All tasks deleted."
            ;;
        4)
            echo "Exiting..."
            exit 0
            ;;
        *)
            echo "Invalid option. Enter 1-4."
            ;;
    esac
done

Best Practices

  1. Quote Variables: Always quote variables in conditions (e.g., [ "$var" = "value" ]) to avoid word-splitting if the variable contains spaces.
  2. Prefer [[ ]] for Bash Scripts: Use [[ ]] for better readability, pattern matching, and fewer edge cases (e.g., [[ "$var" == a* ]]).
  3. Use case for Multiple Patterns: case is cleaner than if-elif when checking a variable against many values.
  4. Check Exit Codes: Use $? to check the exit status of commands (e.g., command; if [ $? -eq 0 ]; then ...), but prefer if command; then ... for brevity.
  5. Avoid Nested if When Possible: Use case or logical operators to simplify nested conditions.

Conclusion

Conditional statements are the backbone of dynamic Bash scripts, enabling decision-making based on file properties, user input, and system state. By mastering if, if-else, if-elif-else, case, and operators like [ ]/[[ ]], you can write scripts that handle edge cases, validate inputs, and automate complex workflows.

Practice with real-world examples (like the ones above) to build intuition, and remember to prioritize readability and portability. With these tools, you’ll be well on your way to writing robust Bash scripts!

References