dotlinux guide

Conditionals and Loops in Shell Scripting: A Tutorial

Table of Contents

Conditionals in Shell Scripting

Conditionals allow scripts to make decisions based on whether a statement is “true” or “false.” They enable logic like: “If a file exists, back it up; otherwise, create it.” or “If the user enters ‘yes’, proceed; else, exit.”

If Statements

The if statement is the most basic conditional construct. It executes a block of code only if a specified condition is met.

Syntax:

if [ condition ]; then
  # Code to run if condition is true
fi
  • [ condition ]: A test expression (note the spaces around [ and ]—this is critical!). The [ symbol is an alias for the test command, which evaluates the condition.
  • then: Marks the start of the code block to execute when the condition is true.
  • fi: Closes the if statement (reverse of if).

Example: Check if a File Exists

#!/bin/bash

file="example.txt"

if [ -f "$file" ]; then  # -f checks if the file exists and is a regular file
  echo "$file exists."
fi

Output:
If example.txt exists: example.txt exists.
If not: No output.

If-Else Statements

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

Syntax:

if [ condition ]; then
  # Code if condition is true
else
  # Code if condition is false
fi

Example: Check File Existence and Create if Missing

#!/bin/bash

file="example.txt"

if [ -f "$file" ]; then
  echo "$file exists."
else
  echo "$file does not exist. Creating it..."
  touch "$file"  # Create the file
fi

Output (if file missing):
example.txt does not exist. Creating it...

If-Elif-Else Chains

For multiple conditions, use if-elif-else (“elif” = “else if”). The first true condition triggers its block; subsequent conditions are ignored.

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

#!/bin/bash

read -p "Enter a number: " num

if [ "$num" -gt 0 ]; then  # -gt = greater than
  echo "Positive number."
elif [ "$num" -lt 0 ]; then  # -lt = less than
  echo "Negative number."
else
  echo "Zero."
fi

Output:
If input is 5: Positive number.
If input is -3: Negative number.
If input is 0: Zero.

Case Statements

For checking a variable against multiple fixed values (e.g., menu options), case statements are cleaner than long if-elif chains.

Syntax:

case "$variable" in
  pattern1)
    # Code if variable matches pattern1
    ;;  # Terminate the pattern block
  pattern2)
    # Code if variable matches pattern2
    ;;
  *)  # Default case (matches any value not covered above)
    # Code for default
    ;;
esac  # Closes the case statement (reverse of "case")

Example: Simple Menu System

#!/bin/bash

echo "Choose an option:"
echo "1. Greet"
echo "2. Farewell"
echo "3. Exit"

read -p "Enter option (1-3): " choice

case "$choice" in
  1)
    echo "Hello!"
    ;;
  2)
    echo "Goodbye!"
    ;;
  3)
    echo "Exiting..."
    exit 0  # Exit the script
    ;;
  *)
    echo "Invalid option."
    ;;
esac

Output (if input is 2): Goodbye!

Loops in Shell Scripting

Loops automate repetitive tasks, such as processing files, iterating over numbers, or repeating commands until a condition is met. Shell scripting supports three primary loop types: for, while, and until.

For Loops

for loops iterate over a list of items (e.g., filenames, numbers, command output) and execute a block of code for each item.

Syntax 1: List Iteration

for item in list; do
  # Code to run for each item
done

Example: Iterate Over a List of Names

#!/bin/bash

names=("Alice" "Bob" "Charlie")

for name in "${names[@]}"; do  # "${names[@]}" expands to all elements of the array
  echo "Hello, $name!"
done

Output:

Hello, Alice!
Hello, Bob!
Hello, Charlie!

Syntax 2: C-Style For Loops (Bash-Specific)

For numerical ranges or complex iteration, use C-style syntax:

for ((initialization; condition; increment)); do
  # Code
done

Example: Count from 1 to 5

#!/bin/bash

for ((i=1; i<=5; i++)); do
  echo "Count: $i"
done

Output:

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

Example: Iterate Over Files

Loop over all .txt files in the current directory:

#!/bin/bash

for file in *.txt; do
  echo "Found text file: $file"
done

Output (if a.txt and b.txt exist):

Found text file: a.txt
Found text file: b.txt

While Loops

while loops execute a block of code as long as a condition is true. They’re useful for repeating actions until a dynamic condition changes (e.g., user input, file creation).

Syntax:

while [ condition ]; do
  # Code to run while condition is true
done

Example: Read Input Until “quit”

#!/bin/bash

echo "Enter text (type 'quit' to exit):"

while read -r input; do  # Read user input into $input
  if [ "$input" = "quit" ]; then
    break  # Exit the loop
  fi
  echo "You entered: $input"
done

Output:

Enter text (type 'quit' to exit):
hello
You entered: hello
world
You entered: world
quit

Until Loops

until loops are the inverse of while loops: they execute code until a condition becomes true. Use them when you need to repeat an action until a target state is reached.

Syntax:

until [ condition ]; do
  # Code to run until condition is true
done

Example: Wait for a File to Exist

#!/bin/bash

target_file="ready.txt"

echo "Waiting for $target_file to be created..."

until [ -f "$target_file" ]; do  # Loop until the file exists
  sleep 1  # Wait 1 second before checking again
done

echo "$target_file found!"

Output (after ready.txt is created in another terminal):

Waiting for ready.txt to be created...
ready.txt found!

Common Practices

To write effective conditionals and loops, follow these widely adopted practices:

1. Use Double Brackets [[ ]] for Enhanced Conditionals

Bash supports [[ ]] (a shell keyword) as an alternative to [ ] (the test command). [[ ]] offers:

  • No need to quote variables (safer for spaces in filenames).
  • Support for pattern matching ([[ "$var" == *txt ]] checks if var ends with “txt”).
  • Logical operators like && (AND) and || (OR) inside the brackets.

Example:

name="document.txt"
if [[ "$name" == *.txt ]]; then  # Pattern matching
  echo "Text file detected."
fi

2. Quote Variables to Handle Spaces

Always quote variables (e.g., "$file") to avoid issues with filenames or input containing spaces. Without quotes, file="my file.txt" would split into my and file.txt in [ -f $file ], causing errors.

Bad: if [ -f $file ]; then ... (fails if $file has spaces)
Good: if [ -f "$file" ]; then ...

3. Indent Code for Readability

Indent loop/conditional blocks (e.g., with 2 or 4 spaces) to make nested logic easier to follow:

# Good
for item in "${list[@]}"; do
  if [ "$item" -gt 10 ]; then
    echo "$item is large"
  fi
done

4. Combine Conditions with Logical Operators

Use && (AND) and || (OR) to chain conditions:

  • if [ -f "$file" ] && [ -r "$file" ]; then ... (File exists and is readable)
  • if [ "$num" -lt 0 ] || [ "$num" -gt 100 ]; then ... (Number is negative or > 100)

Best Practices

To write robust, maintainable shell scripts with conditionals and loops:

1. Avoid Overly Complex Loops

If a loop spans hundreds of lines, refactor logic into functions. For example:

process_file() {
  local file="$1"
  # Logic to process $file
}

# Loop over files and call the function
for file in *.txt; do
  process_file "$file"
done

2. Use set -e for Error Handling

Add set -e at the top of your script to exit immediately if any command fails. This prevents invalid states from propagating:

#!/bin/bash
set -e  # Exit on error

# Script will exit if this command fails (e.g., file not found)
cp important.txt backup/

3. Validate Inputs in Loops

If a loop relies on user input or external data, validate values to avoid crashes. For example, check if a number is numeric before arithmetic operations:

read -p "Enter a number: " num
if ! [[ "$num" =~ ^[0-9]+$ ]]; then  # Regex check for digits
  echo "Error: Not a number."
  exit 1
fi

4. Test with shellcheck

Use ShellCheck (a static analysis tool) to catch bugs in conditionals/loops. It flags issues like unquoted variables, missing then, or invalid operators.

5. Limit Loop Scope with local Variables

In loops inside functions, declare variables as local to avoid polluting the global scope:

count_files() {
  local dir="$1"
  local count=0
  for file in "$dir"/*; do
    count=$((count + 1))  # Increment counter
  done
  echo "Total files: $count"
}

Conclusion

Conditionals (if, case) and loops (for, while, until) are the backbone of dynamic shell scripts. They enable decision-making and repetition, turning simple command sequences into powerful automation tools. By mastering these constructs and following best practices like quoting variables, using [[ ]], and validating inputs, you can write scripts that are efficient, readable, and robust.

To deepen your skills, practice building real-world tools: a backup script with conditional checks, a log parser with loops, or a system monitor with until loops. For further learning, refer to the official Bash documentation and tools like ShellCheck.

References