Lesson 3: Conditionals & Logic

Goal: Learn to make decisions in your scripts with if/else, test operators, and case statements.


Table of Contents

  1. if / else / elif
  2. [Test Commands — [ ] vs [[ ]]](#test-commands----vs--)
  3. File Tests
  4. String Comparisons
  5. Numeric Comparisons
  6. Logical Operators
  7. Case Statements
  8. Short-Circuit Evaluation
  9. Exercises

if / else / elif

Basic if

#!/bin/bash

if [[ -f "/etc/hostname" ]]; then
    echo "Hostname file exists"
fi

if / else

#!/bin/bash
AGE=25

if [[ $AGE -ge 18 ]]; then
    echo "You are an adult"
else
    echo "You are a minor"
fi

if / elif / else

#!/bin/bash
SCORE="${1:-0}"

if [[ $SCORE -ge 90 ]]; then
    echo "Grade: A"
elif [[ $SCORE -ge 80 ]]; then
    echo "Grade: B"
elif [[ $SCORE -ge 70 ]]; then
    echo "Grade: C"
elif [[ $SCORE -ge 60 ]]; then
    echo "Grade: D"
else
    echo "Grade: F"
fi

Test Commands — [ ] vs [[ ]]

Single brackets [ ] — POSIX compatible

# Works in sh and bash
if [ -f "/etc/passwd" ]; then
    echo "File exists"
fi
# Bash only — safer and more features
if [[ -f "/etc/passwd" ]]; then
    echo "File exists"
fi

Why prefer [[ ]]?

VAR=""

# [ ] — FAILS with empty variable (syntax error)
if [ $VAR == "hello" ]; then echo "match"; fi
# Error: [: ==: unary operator expected

# [[ ]] — WORKS even with empty variable
if [[ $VAR == "hello" ]]; then echo "match"; fi
# No error, just false

# [[ ]] supports regex
if [[ "hello123" =~ ^hello[0-9]+$ ]]; then
    echo "Matches!"
fi

# [[ ]] supports pattern matching
if [[ "hello.txt" == *.txt ]]; then
    echo "It's a text file"
fi

Rule: Use [[ ]] in bash scripts. Use [ ] only in #!/bin/sh scripts.


File Tests

#!/bin/bash
FILE="/etc/passwd"
DIR="/var/log"
LINK="/usr/bin/python3"

# Does the file exist?
if [[ -f "$FILE" ]]; then
    echo "$FILE exists and is a regular file"
fi

# Does the directory exist?
if [[ -d "$DIR" ]]; then
    echo "$DIR exists and is a directory"
fi

# Does anything exist at this path?
if [[ -e "$FILE" ]]; then
    echo "$FILE exists (file, directory, or link)"
fi

# Is the file readable?
if [[ -r "$FILE" ]]; then
    echo "$FILE is readable"
fi

# Is the file writable?
if [[ -w "$FILE" ]]; then
    echo "$FILE is writable"
fi

# Is the file executable?
if [[ -x "/usr/bin/ls" ]]; then
    echo "ls is executable"
fi

# Is the file non-empty?
if [[ -s "$FILE" ]]; then
    echo "$FILE is not empty"
fi

# Is it a symbolic link?
if [[ -L "$LINK" ]]; then
    echo "$LINK is a symbolic link"
fi

Practical: Check before acting

#!/bin/bash
set -euo pipefail

CONFIG_FILE="/etc/myapp/config.yaml"
LOG_DIR="/var/log/myapp"

# Ensure config exists
if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "Error: Config file not found: $CONFIG_FILE" >&2
    exit 1
fi

# Create log directory if missing
if [[ ! -d "$LOG_DIR" ]]; then
    mkdir -p "$LOG_DIR"
    echo "Created log directory: $LOG_DIR"
fi

String Comparisons

#!/bin/bash
STR1="hello"
STR2="world"
EMPTY=""

# Equal
if [[ "$STR1" == "hello" ]]; then
    echo "String equals hello"
fi

# Not equal
if [[ "$STR1" != "$STR2" ]]; then
    echo "Strings are different"
fi

# Empty string
if [[ -z "$EMPTY" ]]; then
    echo "Variable is empty"
fi

# Non-empty string
if [[ -n "$STR1" ]]; then
    echo "Variable is not empty"
fi

# Pattern matching (glob)
if [[ "$STR1" == h* ]]; then
    echo "Starts with 'h'"
fi

# Regex matching
if [[ "$STR1" =~ ^[a-z]+$ ]]; then
    echo "Only lowercase letters"
fi

Practical: Validate input

#!/bin/bash
set -euo pipefail

EMAIL="${1:-}"

if [[ -z "$EMAIL" ]]; then
    echo "Usage: $0 <email>" >&2
    exit 1
fi

if [[ "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "Valid email: $EMAIL"
else
    echo "Invalid email: $EMAIL" >&2
    exit 1
fi

Numeric Comparisons

Use -eq, -ne, -lt, -le, -gt, -ge for numbers:

#!/bin/bash
A=10
B=20

if [[ $A -eq $B ]]; then echo "A equals B"; fi
if [[ $A -ne $B ]]; then echo "A not equal B"; fi
if [[ $A -lt $B ]]; then echo "A less than B"; fi
if [[ $A -le $B ]]; then echo "A less or equal B"; fi
if [[ $A -gt $B ]]; then echo "A greater than B"; fi
if [[ $A -ge $B ]]; then echo "A greater or equal B"; fi

Alternative: (( )) for arithmetic

A=10
B=20

if (( A < B )); then echo "A is less than B"; fi
if (( A == B )); then echo "A equals B"; fi
if (( A >= 5 && A <= 15 )); then echo "A is between 5 and 15"; fi

Practical: Disk space check

#!/bin/bash
set -euo pipefail

THRESHOLD=80
USAGE=$(df -h / | awk 'NR==2 {gsub(/%/,""); print $5}')

if (( USAGE >= THRESHOLD )); then
    echo "WARNING: Disk usage is ${USAGE}% (threshold: ${THRESHOLD}%)"
    exit 1
else
    echo "OK: Disk usage is ${USAGE}%"
fi

Logical Operators

AND (&&)

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

# Inside [[ ]]
if [[ -f "$FILE" && -r "$FILE" ]]; then
    echo "File exists AND is readable"
fi

OR (||)

if [[ "$ENV" == "dev" ]] || [[ "$ENV" == "staging" ]]; then
    echo "Non-production environment"
fi

# Inside [[ ]]
if [[ "$ENV" == "dev" || "$ENV" == "staging" ]]; then
    echo "Non-production environment"
fi

NOT (!)

if [[ ! -f "$FILE" ]]; then
    echo "File does NOT exist"
fi

if ! command -v docker &>/dev/null; then
    echo "Docker is not installed"
fi

Case Statements

case is cleaner than long if/elif chains for matching patterns.

Basic case

#!/bin/bash
FRUIT="${1:-}"

case "$FRUIT" in
    apple)
        echo "It's an apple"
        ;;
    banana|plantain)
        echo "It's a banana (or plantain)"
        ;;
    *)
        echo "Unknown fruit: $FRUIT"
        ;;
esac

Pattern matching in case

#!/bin/bash
FILE="${1:-}"

case "$FILE" in
    *.tar.gz|*.tgz)
        echo "Extracting gzip tarball..."
        tar -xzf "$FILE"
        ;;
    *.tar.bz2)
        echo "Extracting bzip2 tarball..."
        tar -xjf "$FILE"
        ;;
    *.zip)
        echo "Extracting zip..."
        unzip "$FILE"
        ;;
    *.gz)
        echo "Decompressing gzip..."
        gunzip "$FILE"
        ;;
    *)
        echo "Unknown format: $FILE"
        exit 1
        ;;
esac

Practical: Docker environment selection

#!/bin/bash
set -euo pipefail

ENV="${APP_ENV:-development}"

case "$ENV" in
    development|dev)
        echo "Starting in development mode..."
        export DEBUG=true
        export LOG_LEVEL=debug
        ;;
    staging)
        echo "Starting in staging mode..."
        export DEBUG=false
        export LOG_LEVEL=info
        ;;
    production|prod)
        echo "Starting in production mode..."
        export DEBUG=false
        export LOG_LEVEL=warn
        ;;
    *)
        echo "Unknown environment: $ENV" >&2
        echo "Valid: development, staging, production" >&2
        exit 1
        ;;
esac

echo "Debug: $DEBUG, Log level: $LOG_LEVEL"

Short-Circuit Evaluation

&& — Run if previous succeeds

[[ -f "config.yaml" ]] && echo "Config found"

command -v docker &>/dev/null && echo "Docker is installed"

mkdir -p /tmp/test && echo "Directory created"

|| — Run if previous fails

[[ -f "config.yaml" ]] || echo "Config NOT found"

command -v docker &>/dev/null || echo "Docker NOT installed"

cd /nonexistent || { echo "Failed to cd"; exit 1; }

Combined pattern

# If-then-else in one line
[[ -f "$FILE" ]] && echo "exists" || echo "missing"

# Check command exists or install it
command -v jq &>/dev/null || sudo apt install -y jq

Exercises

Exercise 1: File checker

cat > filecheck.sh << 'EOF'
#!/bin/bash
set -euo pipefail

TARGET="${1:-}"

if [[ -z "$TARGET" ]]; then
    echo "Usage: $0 <path>"
    exit 1
fi

if [[ -d "$TARGET" ]]; then
    echo "'$TARGET' is a directory"
    echo "  Contents: $(ls -1 "$TARGET" | wc -l) items"
elif [[ -f "$TARGET" ]]; then
    echo "'$TARGET' is a file"
    echo "  Size: $(du -h "$TARGET" | cut -f1)"
    echo "  Lines: $(wc -l < "$TARGET")"
    [[ -x "$TARGET" ]] && echo "  Executable: yes" || echo "  Executable: no"
elif [[ -L "$TARGET" ]]; then
    echo "'$TARGET' is a symbolic link → $(readlink "$TARGET")"
else
    echo "'$TARGET' does not exist"
fi
EOF

chmod +x filecheck.sh
./filecheck.sh /etc/passwd
./filecheck.sh /var/log
./filecheck.sh /nonexistent
rm filecheck.sh

Exercise 2: Number classifier

cat > classify.sh << 'EOF'
#!/bin/bash
set -euo pipefail

NUM="${1:-}"

if [[ -z "$NUM" ]]; then
    echo "Usage: $0 <number>"
    exit 1
fi

if ! [[ "$NUM" =~ ^-?[0-9]+$ ]]; then
    echo "Error: '$NUM' is not a valid integer" >&2
    exit 1
fi

if (( NUM > 0 )); then
    echo "$NUM is positive"
elif (( NUM < 0 )); then
    echo "$NUM is negative"
else
    echo "$NUM is zero"
fi

if (( NUM % 2 == 0 )); then
    echo "$NUM is even"
else
    echo "$NUM is odd"
fi
EOF

chmod +x classify.sh
./classify.sh 42
./classify.sh -7
./classify.sh 0
./classify.sh abc
rm classify.sh

Exercise 3: Service checker

cat > service-check.sh << 'EOF'
#!/bin/bash
set -euo pipefail

check_command() {
    local cmd="$1"
    if command -v "$cmd" &>/dev/null; then
        echo "  [OK] $cmd is installed ($(which "$cmd"))"
    else
        echo "  [MISSING] $cmd is NOT installed"
    fi
}

echo "=== System Check ==="
check_command git
check_command docker
check_command node
check_command python3
check_command nginx
check_command curl
echo "=== Done ==="
EOF

chmod +x service-check.sh && ./service-check.sh
rm service-check.sh

Key Takeaways

  • Use [[ ]] in bash (not [ ]) — safer, more features
  • File tests: -f (file), -d (dir), -e (exists), -x (executable)
  • Strings: ==, !=, -z (empty), -n (not empty), =~ (regex)
  • Numbers: -eq, -ne, -lt, -gt, -le, -ge or use (( ))
  • case for multi-value matching — cleaner than if/elif chains
  • && / || for short-circuit one-liners
  • Always quote variables in conditions: "$var"

Next Lesson: Lesson 4: Loops & Iteration →