Bash Control Structures - If, Loops, and Case Statements
Quick Answer: Use Control Structures to Direct Script Flow
Control structures like if/else statements, loops (for, while, until), and case statements let your scripts make decisions and repeat tasks automatically. The simplest approach is to use if [ condition ]; then ... fi for conditionals and for item in list; do ... done for loops.
Quick Comparison: Which Control Structure Should You Use?
| Structure | Purpose | Best For | Complexity |
|---|---|---|---|
| if/else | Conditional execution | Single or few conditions | Very simple |
| case | Multiple options | Many distinct choices | Simple |
| for loop | Iterate fixed number of times | Lists, ranges, files | Simple |
| while loop | Repeat while condition true | Unknown count, user input | Moderate |
| until loop | Repeat until condition true | Opposite of while | Moderate |
Bottom line: Use if/else for simple conditions, case for many options, for for known iterations, and while for dynamic loops.
Conditional Statements: Making Your Script Decide
Conditionals let your script respond to different situations. The basic syntax is straightforward: test a condition, and if it’s true, run some code. If you need to handle the false case too, add an else block. When you have multiple possibilities, use elif (else if) to chain conditions together without nesting.
Basic If Statement
The simplest conditional checks one thing. Here’s what it looks like:
age=25
if [ $age -ge 18 ]; then
echo "You are an adult"
fi
The condition $age -ge 18 means “greater than or equal to 18”.
If/Else Statement
age=15
if [ $age -ge 18 ]; then
echo "You are an adult"
else
echo "You are a minor"
fi
If/Elif/Else Statement
When you have multiple ranges or categories to check, use elif (else if) to add more conditions. This is cleaner than nested if statements:
score=85
if [ $score -ge 90 ]; then
echo "Grade: A"
elif [ $score -ge 80 ]; then
echo "Grade: B"
elif [ $score -ge 70 ]; then
echo "Grade: C"
else
echo "Grade: F"
fi
# Output: Grade: B
The script checks each condition in order and stops at the first one that’s true. This is much more readable than writing if [ $score -lt 90 ] && [ $score -ge 80 ] in nested conditions.
When to Use If/Else
Use if/else when:
- You need simple conditional logic
- Testing one or two conditions
- You want straightforward sequential decision-making
- Readability matters more than brevity
Avoid if/else when:
- You have many discrete options (use case instead)
- Conditions are complex boolean expressions
- You need pattern matching (use case with wildcards)
Test Operators
The square brackets [ ] are actually a command that tests conditions. Here are the most common operators you’ll use, organized by what you’re testing.
Numeric Comparison (use these with numbers):
[ $a -eq $b ] # Equal
[ $a -ne $b ] # Not equal
[ $a -lt $b ] # Less than
[ $a -le $b ] # Less than or equal
[ $a -gt $b ] # Greater than
[ $a -ge $b ] # Greater than or equal
These operators (-eq, -lt, -gt) are for comparing numbers. Don’t use them for strings—the names are a bit odd, but they stand for “equal,” “less than,” “greater than” etc.
String Comparison (use these with text):
[ "$str1" = "$str2" ] # Equal (single =)
[ "$str1" != "$str2" ] # Not equal
[ -z "$str" ] # Empty string (z = zero length)
[ -n "$str" ] # Not empty (n = non-zero)
[ "$str1" > "$str2" ] # Lexicographically greater
Notice the single = for string comparison (double == works in Bash but = is more portable). Always quote your string variables to avoid word splitting gotchas.
File Tests (checking file properties):
[ -f "$file" ] # File exists and is regular file
[ -d "$dir" ] # Directory exists
[ -e "$path" ] # Path exists (file, dir, link, etc.)
[ -r "$file" ] # Readable by current user
[ -w "$file" ] # Writable by current user
[ -x "$file" ] # Executable or searchable
[ -s "$file" ] # File exists and is not empty
These file tests are invaluable for making scripts robust. You can check if a file exists before trying to read it, or ensure it’s writable before appending to it.
For Loops: Iterating Over Known Collections
For loops run once for each item in a list. They’re perfect when you know the items upfront or want to process a specific set of files. Bash offers several flavors of for loops depending on what you’re iterating over.
Loop Over List
The simplest for loop takes a literal list of items separated by spaces:
for fruit in apple banana orange; do
echo "Fruit: $fruit"
done
# Output:
# Fruit: apple
# Fruit: banana
# Fruit: orange
This runs the code block once for each word in the list. The variable fruit gets the current item on each iteration. You can use this same pattern with any whitespace-separated list.
Loop Over Array
When your items are already in an array variable, expand it with "${array[@]}". The quotes prevent word splitting issues with items containing spaces:
colors=(red green blue)
for color in "${colors[@]}"; do
echo "Color: $color"
done
Always quote "${array[@]}" to handle array elements with spaces correctly.
Loop Over Files
One of the most practical uses is looping through files matching a pattern. This approach is much safer than trying to parse the output of ls:
# Loop through all text files
for file in *.txt; do
echo "Processing: $file"
cat "$file"
done
The *.txt pattern expands to all matching files in the current directory. If no files match, the variable gets the literal string *.txt (unless you use shopt -s nullglob).
Loop Over Range
For numeric ranges, use brace expansion {start..end}. This is handy for simple counting:
# Using brace expansion
for i in {1..5}; do
echo "Number: $i"
done
# Output:
# Number: 1
# Number: 2
# ...
# Number: 5
You can also do ranges with steps: {1..10..2} counts 1, 3, 5, 7, 9.
C-Style For Loop
If you need more control (changing the increment, breaking early), use the C-style syntax with arithmetic:
for ((i=0; i<5; i++)); do
echo "Index: $i"
done
# Output:
# Index: 0
# Index: 1
# Index: 2
# Index: 3
# Index: 4
This gives you the flexibility of traditional programming languages. Notice the double parentheses and the arithmetic operators.
For Loop with Step
You can increment by amounts other than 1, which is useful for skipping or processing alternating items:
# Increment by 2
for ((i=0; i<10; i+=2)); do
echo "Number: $i"
done
# Output: 0 2 4 6 8
When to Use For Loops
Use for loops when:
- You have a known, fixed number of iterations
- Processing items in a list or array
- Working with files matching a pattern
- You know the start and end points (for numeric ranges)
Avoid for loops when:
- The loop count depends on external conditions (use while instead)
- You’re waiting for user input (use while)
- You need to loop indefinitely (use while true)
While Loops: Repeating Until a Condition Changes
While loops keep running as long as a condition remains true. This is your go-to when the number of iterations depends on something external—like user input, file content, or a changing value.
Basic While Loop
The simplest while loop increments a counter until it reaches a limit:
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# Output:
# Count: 1
# Count: 2
# ...
# Count: 5
The condition [ $count -le 5 ] is checked before each iteration. When it becomes false, the loop exits. Don’t forget to increment or change something inside the loop, or you’ll get an infinite loop.
Read File with While Loop
One of the most practical patterns is reading a file line by line. This is safer than piping to a loop because the loop runs in the same shell:
while IFS= read -r line; do
echo "Line: $line"
done < filename.txt
The IFS= prevents trimming of leading/trailing whitespace, and -r prevents backslash interpretation. This is the right way to process each line of a file.
While Loop with Condition
When you need to wait for user input or a specific condition, while loops shine:
answer="no"
while [ "$answer" != "yes" ]; do
read -p "Do you want to continue? " answer
done
echo "Continuing..."
This keeps prompting until the user types “yes”. Real-world scripts often use this for confirmations or retries.
While True Loop (Infinite Loops)
You can create an infinite loop and break out with a condition:
# Infinite loop (use with caution)
while true; do
echo "This runs forever"
sleep 1
done
# Stop with Ctrl+C or break statement
Use while true when you want a loop that runs until explicitly broken. Server daemons and monitoring scripts often use this pattern.
When to Use While Loops
Use while loops when:
- Loop count depends on conditions or user input
- Reading from files or streams line-by-line
- Retrying operations until success
- Monitoring or waiting for events
- Building daemons or long-running services
Avoid while loops when:
- You have a fixed iteration count (use for instead—it’s more efficient)
- Processing a list of known items (for is clearer)
Until Loops: The Opposite of While
Until loops are the mirror image of while loops. They keep running as long as the condition is false, and exit when it becomes true. They’re useful when it’s more natural to think about “until this happens” rather than “while this is true.”
Basic Until Loop
Here’s the fundamental pattern—it looks like while but thinks in reverse:
count=1
until [ $count -gt 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# Same output as while [ $count -le 5 ]
Notice the condition [ $count -gt 5 ] means “greater than 5”. With until, the loop runs while this is false (i.e., while count is NOT greater than 5), and exits when it becomes true. This might feel backwards at first, but it’s just a matter of how you naturally think about the problem.
Until vs While Comparison
| Aspect | While | Until |
|---|---|---|
| Keeps running | While condition is TRUE | While condition is FALSE |
| Syntax | while [ condition ]; | until [ condition ]; |
| Exits when | Condition becomes FALSE | Condition becomes TRUE |
| Best for | ”Keep doing X while Y is true" | "Keep doing X until Y happens” |
When to Use Until
Use until when:
- The condition reads more naturally as “until something happens”
- Retrying until success (easier to read than while with negation)
- Waiting for a process to complete
- Loop until a file appears or becomes available
Avoid until when:
- The condition is naturally expressed as “while X is true”
- You need other programmers to read your code (while is more familiar)
Case Statements: Handling Multiple Discrete Options
Case statements are perfect when you have many different options to choose from. Instead of chaining multiple if/elif/elif... statements, a case statement is cleaner, faster, and more maintainable. Think of it as a switch statement for handling categories.
Basic Case Statement
The fundamental pattern matches a value against multiple patterns:
day="Monday"
case $day in
Monday)
echo "Start of work week"
;;
Friday)
echo "Almost weekend!"
;;
Saturday|Sunday)
echo "Weekend!"
;;
*)
echo "Regular day"
;;
esac
# Output: Start of work week
Each pattern ends with ), the commands follow, and ;; terminates that branch. The * is the default case (catch-all). Notice how Saturday|Sunday matches either value—this is much cleaner than writing multiple separate patterns.
Case with Wildcards (File Matching)
Case statements can match patterns, making them excellent for checking file types or extensions:
filename="document.txt"
case $filename in
*.txt)
echo "Text file"
;;
*.pdf)
echo "PDF file"
;;
*.sh)
echo "Shell script"
;;
*)
echo "Unknown file type"
;;
esac
# Output: Text file
The *.txt pattern matches anything ending in .txt. This is exactly what you’d want when routing files to different handlers based on their extension.
Case with Regular Expression Patterns
You can use more complex patterns with character classes:
input="Hello"
case $input in
[hH]*)
echo "Starts with h or H"
;;
[aeiou]*)
echo "Starts with vowel"
;;
*)
echo "Other"
;;
esac
# Output: Starts with h or H
The pattern [hH]* means “starts with either h or H followed by anything.” This kind of pattern matching is powerful for parsing command arguments or user input.
When to Use Case Statements
Use case when:
- You have many distinct options (more than 2-3)
- Options are simple values or patterns
- You want clean, readable code
- Checking file types, command arguments, or user choices
Avoid case when:
- Conditions are complex boolean expressions (use if instead)
- You only have one or two choices (if/else is simpler)
- You need numeric range comparisons
Loop Control: Breaking and Continuing
Sometimes you need to exit a loop early or skip an iteration. Bash provides two statements for this: break stops the loop entirely, and continue skips to the next iteration.
Break Statement: Exit Early
Use break to stop looping immediately, regardless of whether your condition would normally continue:
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo "Number: $i"
done
# Output: 1 2 3 4
When i reaches 5, the break statement exits the loop entirely. This is useful for processing lists and stopping when you find what you’re looking for, or when an error occurs.
Continue Statement: Skip to Next Iteration
Use continue to skip the rest of the current iteration and jump to the next one:
for i in {1..5}; do
if [ $i -eq 3 ]; then
continue
fi
echo "Number: $i"
done
# Output: 1 2 4 5 (skips 3)
When i equals 3, the continue statement skips the echo and goes directly to the next iteration. This is handy for skipping invalid items or conditions you don’t want to process.
Nested Loop Control
When you have loops inside loops, break and continue affect the innermost loop:
for outer in {1..3}; do
for inner in {1..3}; do
if [ $inner -eq 2 ]; then
continue # Skips inner loop iteration only
fi
echo "Outer: $outer, Inner: $inner"
done
done
The continue only skips the inner loop iteration, not the outer one. If you need to break out of multiple levels, you can use break 2 to break out of two levels, break 3 for three levels, etc.
Combining Conditions: AND, OR, and NOT
Often you need to check multiple conditions. Bash provides logical operators to combine conditions: AND (&&), OR (||), and NOT (!). These let you build complex decision logic without deeply nested if statements.
AND Operator: Both Must Be True
Use && when both conditions must be true for the action to happen:
if [ -f "$file" ] && [ -r "$file" ]; then
echo "File exists and is readable"
fi
# Or using -a (older syntax, less readable)
if [ -f "$file" -a -r "$file" ]; then
echo "File exists and is readable"
fi
Both && and -a work, but && is more readable and works outside of test brackets. Use && in new code.
OR Operator: At Least One Must Be True
Use || when at least one condition must be true:
if [ ! -f "$file" ] || [ ! -r "$file" ]; then
echo "File missing or not readable"
fi
# Or using -o (older syntax)
if [ ! -f "$file" -o ! -r "$file" ]; then
echo "File missing or not readable"
fi
This checks if the file is missing OR not readable. Again, || is more readable than -o.
NOT Operator: Reverse a Condition
Use ! to reverse the meaning of a condition:
if [ ! -f "$file" ]; then
echo "File does not exist"
fi
The ! operator inverts the test. Instead of checking “if file exists,” you’re checking “if file does NOT exist.” This is clearer than checking equality to false.
Complex Conditions: Combining Multiple Operators
You can chain multiple operators together, and they’re evaluated left-to-right:
if [ $age -ge 18 ] && [ $age -lt 65 ]; then
echo "Working age"
elif [ $age -ge 65 ]; then
echo "Retirement age"
else
echo "Below working age"
fi
This first condition checks if age is both at least 18 AND less than 65. Using separate brackets with && is clearer than combining everything into one bracket.
Best Practices for Control Structures
1. Always Quote Variables
Quoting prevents word splitting and glob expansion. This is the #1 source of bugs in shell scripts:
# Good - handles spaces and special characters
if [ "$var" = "value" ]; then
echo "Match"
fi
# Bad - breaks if $var contains spaces
if [ $var = value ]; then
echo "Match"
fi
Unquoted variables get split on whitespace and expanded as globs. This leads to “too many arguments” errors that are hard to debug.
2. Use [[ ]] for Complex Conditions
The double-bracket syntax [[ ]] is Bash-specific but more robust. It handles quoting better and supports pattern matching directly:
# Good - Bash-specific, handles quoting better
if [[ $var == pat* ]]; then
echo "Match"
fi
# Also works but requires careful quoting
if [ "$var" = "pat"* ]; then
echo "Match"
fi
Use [[ ]] when you’re sure you’re running Bash. Use [ ] when you need POSIX compatibility across different shells.
3. Avoid Deep Nesting: Use Early Exit
Deeply nested conditions are hard to read. Instead, check for failure conditions first and exit early:
# Good - early exit, cleaner flow
if [ ! -f "$file" ]; then
echo "Error: file not found"
return 1
fi
process "$file"
# Bad - deeply nested, hard to follow
if [ -f "$file" ]; then
process "$file"
else
echo "Error: file not found"
return 1
fi
This pattern, called “guard clauses,” makes code easier to follow because the happy path isn’t indented.
4. Use Case for Multiple Options
When you have many discrete choices, case is cleaner and more maintainable than if/elif chains:
# Good - clear structure, easy to add options
case $option in
1) do_one;;
2) do_two;;
*) echo "Invalid option";;
esac
# Bad - repetitive, harder to maintain
if [ "$option" = "1" ]; then
do_one
elif [ "$option" = "2" ]; then
do_two
else
echo "Invalid option"
fi
5. Test File Conditions Before Use
Always check that files and directories exist before trying to use them:
# Good - safe
if [ -r "$input_file" ]; then
cat "$input_file"
else
echo "Error: cannot read file" >&2
return 1
fi
# Risky - fails if file doesn't exist
cat "$input_file"
Quick Reference: Common Control Structure Patterns
Simple if:
if [ condition ]; then
action
fi
If/else:
if [ condition ]; then
action1
else
action2
fi
Loop over items:
for item in list; do
action "$item"
done
Loop with counter:
for ((i=0; i<10; i++)); do
action $i
done
Loop until condition:
while [ condition ]; do
action
update_condition
done
Multiple options:
case $var in
option1) action1;;
option2) action2;;
*) default_action;;
esac
Frequently Asked Questions
Q: When should I use [ vs [[?
A: Use [[ in Bash for better features (pattern matching, regex). Use [ for POSIX compatibility with other shells.
Q: What’s the difference between -a and &&?
A: Both work for AND, but && is more readable and handles precedence better. Prefer && in modern scripts.
Q: How do I loop indefinitely?
A: Use while true; or until false;. Stop with break or press Ctrl+C.
Q: Can I nest control structures?
A: Yes, loops and conditionals can be nested deeply. However, keep nesting shallow (2-3 levels) for readability.
Q: What’s the difference between case and multiple if statements?
A: Case is cleaner and more efficient for matching many distinct values. If/else is better for complex boolean conditions.
Q: How do I break out of multiple nested loops?
A: Use break 2 to exit two levels, break 3 for three, etc. Example: for x in ...; do for y in ...; do break 2; done; done
Summary: Making Smart Script Decisions
Control structures are the backbone of useful Bash scripts. Use if/else for simple conditions, case when you have many options, for loops to process known items, and while loops for dynamic conditions. Remember to:
- Always quote variables to prevent word splitting
- Use file tests (
-f,-d,-r,-w) before trying to use files - Choose the clearest control structure for your situation
- Keep nesting shallow with early exit patterns
- Test your conditions carefully—off-by-one errors are common
Start simple with if/else, then graduate to case statements and loops as your scripts grow more complex. Most real-world scripts combine all these structures, so practice with small examples first.