Skip to content
5 min read·Lesson 4 of 10

Control Flow and Loops

if, case, while, for, and the test syntax that lets Bash make decisions.

Bash control flow looks unfamiliar at first because conditions are commands. An if runs a command and branches on its exit code. Once you internalise that, the rest follows.

Exit Codes

true; echo $?       # 0  — success
false; echo $?      # 1  — failure
ls /nonexistent; echo $?   # >0 — failure

Every command has an exit code. 0 means success; non-zero means failure. if branches on this directly.

if

if grep -q error /var/log/app.log; then
  echo "errors found"
fi

if [ -f /etc/hosts ]; then
  echo "exists"
elif [ -d /etc ]; then
  echo "dir but no file"
else
  echo "neither"
fi

[ is the test command — really /usr/bin/test. [[ is a Bash builtin with safer semantics.

The Test Command

[[ -f file ]]              # regular file exists
[[ -d dir ]]               # directory exists
[[ -e path ]]              # any kind of path exists
[[ -r file ]]              # readable
[[ -w file ]]              # writable
[[ -x file ]]              # executable
[[ -s file ]]              # non-empty file
[[ -L link ]]              # symbolic link

[[ -z "$VAR" ]]            # empty string
[[ -n "$VAR" ]]            # non-empty string
[[ "$A" == "$B" ]]         # string equal
[[ "$A" != "$B" ]]         # string not equal
[[ "$A" =~ ^[0-9]+$ ]]     # regex match

[[ "$N" -eq 5 ]]           # numeric equal
[[ "$N" -ne 5 ]]           # not equal
[[ "$N" -lt 5 ]]           # less than
[[ "$N" -le 5 ]]           # less or equal
[[ "$N" -gt 5 ]]           # greater
[[ "$N" -ge 5 ]]           # greater or equal

(( N == 5 ))               # alternative: arithmetic context
(( N > 5 ))

Use (( ... )) for numeric, [[ ... ]] for strings and files. Avoid [ ... ] in new code.

Combining Conditions

if [[ -f "$FILE" && -r "$FILE" ]]; then
  echo "readable file"
fi

if [[ -d /tmp || -d /var/tmp ]]; then
  echo "temp exists"
fi

if ! [[ -f /etc/hosts ]]; then
  echo "missing"
fi

case

case "$1" in
  start)
    echo "starting..."
    ;;
  stop)
    echo "stopping..."
    ;;
  restart|reload)
    echo "restarting..."
    ;;
  *)
    echo "Usage: $0 {start|stop|restart}"
    exit 1
    ;;
esac

Cleaner than nested if for matching strings or globs. Patterns can include *, ?, and character classes.

for Loops

# Over a list
for env in dev staging prod; do
  echo "Deploying to $env"
done

# Over command output (PROBLEMATIC if filenames have spaces)
for f in $(ls *.log); do
  echo "$f"
done

# Over an array (RECOMMENDED)
FILES=(*.log)
for f in "${FILES[@]}"; do
  echo "$f"
done

# Over a range
for i in {1..10}; do
  echo "$i"
done

# C-style
for ((i=0; i<10; i++)); do
  echo "$i"
done

while Loops

i=0
while (( i < 5 )); do
  echo "$i"
  ((i++))
done

# Until — opposite condition
n=10
until (( n == 0 )); do
  echo "$n"
  ((n--))
done

# Read a file line by line (the right way)
while IFS= read -r line; do
  echo "> $line"
done < input.txt

IFS= read -r is the safe form — preserves leading/trailing whitespace and backslashes. The wrong form (read line) eats both.

Pipelines and Loops

Be careful: a pipeline runs each stage in a subshell, so variables set inside the loop do not survive.

cat file.txt | while read line; do
  count=$((count + 1))
done
echo "$count"     # always 0 or empty

# Use process substitution instead:
count=0
while read line; do
  count=$((count + 1))
done < <(cat file.txt)
echo "$count"     # actually counts

Break and Continue

for i in {1..10}; do
  if (( i == 5 )); then continue; fi
  if (( i == 8 )); then break; fi
  echo "$i"
done

A Worked Example

#!/usr/bin/env bash
set -euo pipefail

LOG_DIR="${1:-/var/log}"
DAYS="${2:-7}"

if [[ ! -d "$LOG_DIR" ]]; then
  echo "Not a directory: $LOG_DIR" >&2
  exit 1
fi

echo "Cleaning files older than $DAYS days in $LOG_DIR"
count=0
while IFS= read -r -d '' file; do
  rm -f "$file"
  ((count++))
done < <(find "$LOG_DIR" -type f -name "*.log" -mtime +"$DAYS" -print0)

echo "Removed $count files"

Notes: set -euo pipefail for safety (next lesson explains), -print0 + read -d '' to handle filenames with spaces or newlines, defaults via ${1:-/var/log}.

Cert Mapping

CertScope
RHCSA / LFCSScripting tasks on the practical exam
AWS / AzureUser data, init scripts, automation runbooks

The next lesson covers what makes the shell uniquely powerful: the pipe.

Key Takeaways

  • if uses the exit code of a command — true is 0, not 1.
  • [[ ... ]] is the modern test command; prefer it over [ ... ].
  • case is cleaner than chained if for matching strings.
  • for, while, and until cover all looping needs.
  • Always quote loop variables, especially with filenames.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →