Skip to content

Control Flow

if/elif/else and case statements are what turn a sequence of commands into a script that makes decisions — the foundation of any real automation.

Learning Objectives

  • Write if/elif/else statements using [](<#>) and (( ))
  • Use file and string test operators correctly
  • Write case statements for multi-branch logic
  • Understand exit codes and how they drive conditional logic

Exit Codes

Every command returns an exit code: 0 means success, non-zero means failure. This is the foundation of all conditional logic in bash.

ls /etc
echo $?          # 0 — success
0

ls /nonexistent
echo $?          # non-zero — failure
2

The rule: 0 = true, non-zero = false

In bash conditions, 0 is treated as true and any non-zero value as false. This is the opposite of most other languages. It makes sense because 0 means "no error."


if Statements

if [[ condition ]]; then
    # commands if true
elif [[ other_condition ]]; then
    # commands if elif true
else
    # commands if none matched
fi

File Tests

if [-f "/etc/passwd"](<../-f "/etc/passwd".md>); then
    echo "File exists"
fi

if [-d "$HOME/scripts"](<../-d "$HOME/scripts".md>); then
    echo "Directory exists"
fi

if [! -e "/tmp/lockfile"](<../! -e "/tmp/lockfile".md>); then
    echo "Lock file does not exist"
fi

Common file test operators:

Operator True if
-f file file exists and is a regular file
-d dir directory exists
-e path path exists (any type)
-r file file exists and is readable
-w file file exists and is writable
-x file file exists and is executable
-s file file exists and has size > 0
-L file file is a symbolic link

String Tests

name="Nikhil"

if [[ -z "$name" ]]; then
    echo "Name is empty"
elif [[ -n "$name" ]]; then
    echo "Name is: $name"
fi

if [[ "$name" == "Nikhil" ]]; then
    echo "Hello, Nikhil"
fi

if [[ "$name" != "root" ]]; then
    echo "Not running as root"
fi

# Pattern matching with ==
if [[ "$filename" == *.log ]]; then
    echo "This is a log file"
fi

Numeric Tests

Use (( )) for arithmetic comparisons — it is cleaner than [](<#>) for numbers:

count=42

if (( count > 10 )); then
    echo "More than 10"
fi

if (( count >= 40 && count <= 50 )); then
    echo "Between 40 and 50"
fi

Or use -eq, -ne, -lt, -le, -gt, -ge inside [](<#>):

if [[ "$count" -gt 10 ]]; then
    echo "More than 10"
fi

[](<#>) vs [ ] vs (( ))

Use [](<#>) for string and file tests — it is a bash keyword, safer, and supports &&, ||, and pattern matching. Use (( )) for arithmetic. Avoid [ ] (POSIX test) in bash scripts — it has subtler quoting rules and fewer features.


Combining Conditions

if [[ -f "$file" && -r "$file" ]]; then
    echo "File exists and is readable"
fi

if [[ -z "$1" || "$1" == "--help" ]]; then
    echo "Usage: $0 <filename>"
    exit 0
fi

case Statements

case is cleaner than a long if/elif chain when you are matching a single value against multiple patterns:

case "$1" in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart)
        echo "Restarting service..."
        ;;
    --help|-h)
        echo "Usage: $0 {start|stop|restart}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac
# case with glob patterns
case "$filename" in
    *.jpg|*.jpeg|*.png)
        echo "Image file"
        ;;
    *.mp3|*.wav|*.flac)
        echo "Audio file"
        ;;
    *.sh)
        echo "Shell script"
        ;;
    *)
        echo "Unknown file type"
        ;;
esac

Practical Pattern: Argument Checking

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

if [[ $# -eq 0 ]]; then
    echo "Usage: $0 <directory>" >&2
    exit 1
fi

DIR="$1"

if [[ ! -d "$DIR" ]]; then
    echo "Error: '$DIR' is not a directory" >&2
    exit 1
fi

echo "Processing: $DIR"

Common Mistakes

Missing quotes around variables in tests

[[ $name == "Nikhil" ]] works because [](<#>) does not word-split. But [ $name == "Nikhil" ] fails if $name is empty or contains spaces. Always quote variable references even inside [](<#>) — it is a good habit.

Using = vs == in [](<#>)

Both = and == work for string equality inside [](<#>). Use == for clarity. Do not use == inside [ ] — there, you must use =.


Practice Exercises

Warm-Up (run and observe)

  1. Run [[ "hello" == "hello" ]]; echo $?. Then try [[ "hello" == "world" ]]; echo $?. What exit codes do you see?
  2. Run [-f /etc/passwd](<../-f /etc/passwd.md>); echo $? and [-f /nonexistent](<../-f /nonexistent.md>); echo $?. What do the results mean?
  3. Write a one-liner that prints "root" if $USER equals "root" and "not root" otherwise.

Main (write a short script)

Create ~/scripts/file_type.sh:

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

FILE="${1:?Usage: $0 <path>}"

if [[ ! -e "$FILE" ]]; then
    echo "Error: '$FILE' does not exist" >&2
    exit 1
fi

if [[ -d "$FILE" ]]; then
    echo "'$FILE' is a directory"
    echo "Contains $(ls "$FILE" | wc -l) items"
elif [[ -L "$FILE" ]]; then
    echo "'$FILE' is a symlink -> $(readlink "$FILE")"
elif [[ -f "$FILE" ]]; then
    echo "'$FILE' is a regular file"
    echo "Size: $(du -sh "$FILE" | cut -f1)"
    echo "Lines: $(wc -l < "$FILE")"
fi

Stretch

  1. Write a script that checks if a given port is in your /etc/services file and prints its protocol.
  2. Rewrite the file_type.sh using a case statement instead of if/elif.
  3. What happens if you write if [ -z $unset_var ] when unset_var is not defined? How does adding quotes fix it? How does set -u change the behavior?

Interview Questions

  1. What exit code does a successful command return, and why is 0 "true" in bash?
Show answer

A successful command returns 0. In bash, if command runs the then block if the command exits with 0 — this is "true." Non-zero means failure ("false"). This is the opposite of C, Python, etc. It makes sense because 0 means "no error" — there is only one way to succeed but many ways to fail (different non-zero error codes).

  1. What is the difference between [](<#>) and [ ]?
Show answer

[](<#>) is a bash keyword — it does not perform word-splitting or glob expansion on variable values, supports && and || directly, allows == with glob patterns, and supports regex matching with =~. [ ] is the POSIX test command — it is more portable but has more quoting edge cases. Use [](<#>) in bash scripts.

  1. How do you check if two strings are equal in bash?
Show answer

[[ "$str1" == "$str2" ]] for exact equality. Note: == inside [](<#>) performs glob pattern matching on the right side (unquoted). [[ "$filename" == *.log ]] checks if filename ends in .log. For literal equality when the right side might contain glob characters, quote it: [[ "$str" == "*.log" ]].


day03-part2-user-input-expansion | day04-part2-loops