Conditional logic is the backbone of any non-trivial script. Bash offers if/elif/else for readable multi-branch logic and case for clean pattern matching. Knowing when to use each — and how to write them concisely — is essential for production scripts.
1
if / elif / else — full syntax
BASH
#!/usr/bin/env bash
# Basic if/elif/else structure
score=75
if (( score >= 90 )); then
echo "Grade: A"
elif (( score >= 75 )); then
echo "Grade: B"
elif (( score >= 60 )); then
echo "Grade: C"
else
echo "Grade: F"
fi
# ── String conditions ─────────────────────────────────────
env="production"
if [[ "${env}" == "production" ]]; then
echo "Running in PRODUCTION — extra caution!"
elif [[ "${env}" == "staging" ]]; then
echo "Staging environment"
else
echo "Development / unknown: ${env}"
fi
# ── File conditions ───────────────────────────────────────
config="/etc/myapp/config.env"
if [[ ! -f "${config}" ]]; then
echo "ERROR: Config file missing: ${config}" >&2
exit 1
elif [[ ! -r "${config}" ]]; then
echo "ERROR: Config file not readable: ${config}" >&2
exit 1
else
source "${config}"
echo "Config loaded successfully"
fi
2
case statement — clean multi-branch matching
case is cleaner than long if/elif chains when you're matching a variable against multiple fixed patterns. It supports glob patterns, multiple patterns per branch, and fall-through alternatives.
BASH
# ── Basic case ────────────────────────────────────────────
action="${1:-start}"
case "${action}" in
start)
echo "Starting service..."
systemctl start myapp
;;
stop)
echo "Stopping service..."
systemctl stop myapp
;;
restart)
echo "Restarting service..."
systemctl restart myapp
;;
status)
systemctl status myapp
;;
*)
echo "Usage: $0 {start|stop|restart|status}" >&2
exit 1
;;
esac
# ── Multiple patterns per branch ──────────────────────────
ext="${file##*.}"
case "${ext}" in
jpg|jpeg|png|gif|webp)
echo "Image file"
;;
mp4|mkv|avi|mov)
echo "Video file"
;;
sh|bash|zsh)
echo "Shell script"
;;
csv|tsv|xlsx)
echo "Spreadsheet"
;;
*)
echo "Unknown type: ${ext}"
;;
esac
# ── Glob patterns in case ─────────────────────────────────
version="5.7.42"
case "${version}" in
8.*) echo "MySQL 8.x" ;;
5.7.*) echo "MySQL 5.7" ;;
5.6.*) echo "MySQL 5.6 — EOL!" ;;
*) echo "Unknown MySQL version" ;;
esac
Terminal output
Key
Matched branch
Error / default
Warning branch
vriddh@prod-01:~/scripts$./service.sh start
Starting service...
vriddh@prod-01:~/scripts$./service.sh reload
Usage: service.sh {start|stop|restart|status}
vriddh@prod-01:~/scripts$version=5.7.42 bash version_check.sh
MySQL 5.7
vriddh@prod-01:~/scripts$version=5.6.50 bash version_check.sh
MySQL 5.6 — EOL!
█
3
One-liners — inline conditionals
BASH
# ── && || one-liners ──────────────────────────────────────
[[ -d "/app" ]] && echo "app dir exists"
[[ -f "/app/pid" ]] || echo "app not running"
# Guard — exit immediately on failure
[[ "${EUID}" -eq 0 ]] || { echo "Need root" >&2; exit 1; }
[[ -n "${DB_PASS}" ]] || { echo "DB_PASS not set" >&2; exit 1; }
# ── Ternary simulation ────────────────────────────────────
status=$( [[ "${1}" == "ok" ]] && echo "UP" || echo "DOWN" )
echo "Service: ${status}"
# ── if on one line ────────────────────────────────────────
if [[ -f "/etc/passwd" ]]; then echo "exists"; fi
# ── Conditional assignment ────────────────────────────────
log_dir="/var/log/myapp"
[[ -d "${log_dir}" ]] || mkdir -p "${log_dir}"
# ── Check command availability ────────────────────────────
command -v jq >/dev/null 2>&1 || { echo "jq not installed"; exit 1; }
command -v curl >/dev/null 2>&1 && echo "curl available"
4
Nested conditionals — keep it readable
BASH
#!/usr/bin/env bash
# db_check.sh — Multi-level environment and DB validation
ENV="${APP_ENV:-development}"
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-3306}"
if [[ "${ENV}" == "production" ]]; then
echo "[PROD] Running production checks..."
if [[ "${DB_HOST}" == "localhost" ]]; then
echo "ERROR: Production must not use localhost DB" >&2
exit 1
elif ! nc -z "${DB_HOST}" "${DB_PORT}" >/dev/null 2>&1; then
echo "ERROR: Cannot reach DB ${DB_HOST}:${DB_PORT}" >&2
exit 1
else
echo " DB reachable: ${DB_HOST}:${DB_PORT}"
fi
elif [[ "${ENV}" == "staging" ]]; then
echo "[STAGING] Lighter checks..."
nc -z "${DB_HOST}" "${DB_PORT}" >/dev/null 2>&1 \
&& echo " DB OK" \
|| echo " WARNING: DB not reachable"
else
echo "[DEV] Skipping DB checks for local dev"
fi
echo "Checks complete for ENV=${ENV}"
5
Real-world — deployment gate with full validation
BASH
#!/usr/bin/env bash
# deploy_gate.sh — Pre-deployment validation checks
set -euo pipefail
ERRORS=0
WARNINGS=0
check() {
local label="${1}"
local result="${2}" # "ok", "warn", or "fail"
local msg="${3:-}"
case "${result}" in
ok) printf " ✔ %-30s %s\n" "${label}" "${msg}" ;;
warn) printf " ⚠ %-30s %s\n" "${label}" "${msg}"; (( WARNINGS++ )) ;;
fail) printf " ✘ %-30s %s\n" "${label}" "${msg}"; (( ERRORS++ )) ;;
esac
}
echo "Pre-deployment checks:"
if [[ "${EUID}" -eq 0 ]]; then
check "Running as root" "warn" "Consider using a service account"
else
check "Running as root" "ok" "user: ${USER}"
fi
[[ -n "${APP_VERSION:-}" ]] \
&& check "APP_VERSION set" "ok" "v${APP_VERSION}" \
|| check "APP_VERSION set" "fail" "required"
[[ -f "/app/current/Makefile" ]] \
&& check "Makefile present" "ok" "" \
|| check "Makefile present" "fail" "not found"
echo "─────────────────────────────────"
echo "Errors: ${ERRORS} Warnings: ${WARNINGS}"
if (( ERRORS > 0 )); then
echo "Deployment blocked — fix errors first" >&2
exit 1
fi
echo "All checks passed — proceeding"
Terminal output
vriddh@prod-01:~/scripts$APP_VERSION=2.4.1 ./deploy_gate.sh
Pre-deployment checks:
✔ Running as root user: vriddh
✔ APP_VERSION set v2.4.1
✘ Makefile present not found
─────────────────────────────────
Errors: 1 Warnings: 0
Deployment blocked — fix errors first
█
✔ Best practices — Prefer
case over long if/elif chains when matching one variable against many values. Always include a *) default in every case. Use || { echo ...; exit 1; } for compact guards. Keep nesting to a maximum of 3 levels — extract deeper logic into functions.