Skip to main content
RunBook Academy

LinuxXXXV · Shell Scripting for SysadminsConditions loops

Conditions and loops - controlling script flow

Foundation⏱ ~10 minbash

What you'll learn

  • Use if/elif/else/fi
  • Use case statements for multiple branches
  • Use for and while loops
  • Recognise common patterns

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

Conditions and loops are the basic control structures in bash. This lesson covers the right patterns for production scripts.

if / elif / else / fi

if [[ -f "$FILE" ]]; then
    echo "File exists"
elif [[ -d "$FILE" ]]; then
    echo "Directory"
else
    echo "Neither"
fi

Use [[ ]] (bash builtin; more features than [ ]).

Test operators

# File tests
[[ -f "$file" ]]    # is a regular file
[[ -d "$dir" ]]     # is a directory
[[ -e "$path" ]]    # exists
[[ -r "$file" ]]    # readable
[[ -w "$file" ]]    # writable
[[ -x "$file" ]]    # executable
[[ -s "$file" ]]    # non-empty
[[ -L "$link" ]]    # is a symlink

# String tests
[[ -z "$str" ]]     # empty
[[ -n "$str" ]]     # non-empty
[[ "$a" == "$b" ]]  # equal
[[ "$a" != "$b" ]]  # not equal
[[ "$str" =~ ^foo ]]  # regex match

# Numeric tests
[[ "$n" -eq 5 ]]    # equal
[[ "$n" -gt 5 ]]    # greater than
[[ "$n" -lt 5 ]]    # less than

# Logical
[[ -f "$a" && -f "$b" ]]   # both
[[ -f "$a" || -f "$b" ]]   # either
[[ ! -f "$a" ]]            # not

case statement

For multiple branches:

case "$1" in
    start)   start_service ;;
    stop)    stop_service ;;
    restart) stop_service; start_service ;;
    *)       echo "Usage: $0 {start|stop|restart}" >&2; exit 1 ;;
esac

*) matches anything. Patterns use glob syntax.

for loops

# Iterate over a list
for name in alice bob carol; do
    echo "Hello, $name"
done

# Iterate over files
for file in /var/log/*.log; do
    echo "Processing $file"
done

# C-style for. The i++ here is part of the for construct, not a
# standalone command, so it is safe under set -e.
for ((i = 0; i < 10; i++)); do
    echo "$i"
done

# Iterate over command output
while IFS= read -r line; do
    echo "$line"
done < /etc/passwd

while loops

# Condition-based
count=0
while ((count < 10)); do
    echo "$count"
    count=$((count + 1))
done

# Until condition is true
count=0
until ((count >= 10)); do
    echo "$count"
    count=$((count + 1))
done

# Infinite (with break) - use a real test, not a placeholder word.
# `[[ condition ]]` is a non-empty string, so it is ALWAYS true and the
# loop breaks on the first pass. shellcheck flags it as SC2078.
while true; do
    if [[ -f /var/run/job.done ]]; then
        break
    fi
    sleep 5
done

The same trap applies to the arithmetic conditions above - while ((count < 10)) - but there it is harmless. A command in a while, until or if condition is exempt from set -e by design; that is exactly how the loop terminates. The danger is only the arithmetic command standing alone in the loop body.

Where set -e does not fire

set -euo pipefail is a backstop, not a safety net. It does not trigger when the failing command is:

  1. Any command in an if, while or until condition.
  2. Any command joined by && or ||, except the last one.
  3. Any command negated with !.
  4. An arithmetic command such as ((i++)) that evaluates to 0.
  5. A command inside $( ) whose status is discarded by an assignment - local x=$(cmd) or export x=$(cmd). The assignment’s status is what counts, and it is 0.
  6. Any command in a subshell whose status is not checked, including the left-hand side of a pipeline without pipefail.

Check the statuses that matter explicitly. Do not assume set -e covers them.

Common patterns

Iterate over files safely

count=0
while IFS= read -r -d '' file; do
    echo "Processing $file"
    count=$((count + 1))
done < <(find /var/log -name '*.log' -print0)
echo "processed $count files"

-print0 and -d '' handle filenames with spaces — and with newlines, which is the case that actually bites, because a newline in a filename is indistinguishable from a record separator in line-oriented output.

The done < <(find ...) form is the part worth memorising. The obvious way to write this is a pipeline:

find /var/log -name '*.log' -print0 | while IFS= read -r -d '' file; do
    count=$((count + 1))     # incremented in a SUBSHELL
done
echo "$count"                # still 0

Every stage of a pipeline runs in a subshell, so the loop body gets its own copy of the shell’s variables. The counter goes up inside the loop and is discarded when the loop ends. So does an array you appended to, so does a flag you set — and an exit 1 inside the loop exits the subshell, leaving the script running as if nothing had failed. The script reports success on a run that processed nothing.

Process substitution (< <(cmd)) redirects the command’s output into the loop’s stdin without a pipeline, so the loop runs in the current shell and the variables survive.

One thing it does not give you: the producer’s exit status. find failing halfway through leaves the loop looking like it simply had less work to do. If a truncated run must not pass silently, write the output to a temp file first, check the status, then read the file.

Skip a directory

for file in *; do
    [[ -d "$file" ]] && continue
    echo "File: $file"
done

Read line by line

while IFS= read -r line; do
    echo "$line"
done < input.txt

IFS= prevents trimming; -r prevents escape interpretation.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the difference between [[ ]] and [ ]?

  2. Q2. `for f in *` handles filenames containing spaces correctly, while `for f in $(ls)` does not.

  3. Q3. Which of the following are valid bash test operators? Select all that apply.

  4. Q4. A rolling-restart script starts with set -euo pipefail, sets count=0, and increments with ((count++)) inside a while loop. It restarts node1 and then exits with status 1. What actually happened?

  5. Q5. In which of these situations does set -e NOT abort the script on a failing command? Select all that apply.

Passing score: 75%. Answers are checked in this browser.