Lesson 3: Conditionals & Logic
Goal: Learn to make decisions in your scripts with if/else, test operators, and case statements.
Table of Contents
- if / else / elif
- [Test Commands — [ ] vs [[ ]]](#test-commands----vs--)
- File Tests
- String Comparisons
- Numeric Comparisons
- Logical Operators
- Case Statements
- Short-Circuit Evaluation
- 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
Double brackets [[ ]] — Bash enhanced (recommended)
# 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,-geor use(( )) casefor 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 →