if / case
Bash conditionals come in two flavours: `[[ ... ]]` (modern, safe) and `[ ... ]` (POSIX, fragile). Combine with `&&`, `||`, `if`, `case`, and short-circuit patterns to write scripts that read like prose. Quoting still matters; ShellCheck still catches the rest.
if / case / [[ ]] / && / ||
EXAMPLE
#!/usr/bin/env bash
set -Eeuo pipefail
# ===== 1) Test command: [[ ... ]] vs [ ... ] =====
# Prefer [[ ... ]] — it does not word-split, supports patterns + regex, safer.
name='alice'
if [[ "$name" == 'alice' ]]; then echo 'match'; fi # double bracket
if [ "$name" = 'alice' ]; then echo 'match'; fi # POSIX (single =)
# ===== 2) String comparisons =====
[[ -z "$name" ]] # zero length (empty)
[[ -n "$name" ]] # non-empty
[[ "$name" == 'alice' ]] # equals
[[ "$name" != 'bob' ]] # not equals
[[ "$name" < 'zelda' ]] # lexicographic <
[[ "$name" == a* ]] # glob match
[[ "$name" =~ ^[a-z]+$ ]] # regex match (BASH_REMATCH for captures)
# ===== 3) Numeric comparisons =====
n=5
[[ $n -eq 5 ]] # equals (numeric)
[[ $n -ne 0 ]] # not equals
[[ $n -lt 10 ]] # less than
[[ $n -le 10 ]] # less or equal
[[ $n -gt 0 ]] # greater than
[[ $n -ge 1 ]] # greater or equal
(( n > 0 && n < 100 )) # C-style arithmetic test
# ===== 4) File tests =====
[[ -e /etc/hosts ]] # exists
[[ -f /etc/hosts ]] # regular file
[[ -d /etc ]] # directory
[[ -L /usr/bin/python3 ]] # symlink
[[ -r /etc/hosts ]] # readable
[[ -w /tmp ]] # writable
[[ -x /usr/bin/git ]] # executable
[[ -s /var/log/app.log ]] # exists and size > 0
[[ a.txt -nt b.txt ]] # newer than
[[ a.txt -ot b.txt ]] # older than
# ===== 5) if / elif / else =====
if [[ -d /etc/nginx ]]; then
echo 'nginx present'
elif [[ -d /etc/apache2 ]]; then
echo 'apache present'
else
echo 'no web server config dir'
fi
# ===== 6) case — multi-way branch =====
read -rp 'env > ' env
case "$env" in
dev|development) config='dev.env' ;;
stage|staging) config='stage.env' ;;
prod|production) config='prod.env' ;;
*) echo 'unknown env'; exit 2 ;;
esac
echo "using $config"
# ===== 7) Short-circuit && / || =====
[[ -f config.toml ]] && echo 'have config' || echo 'missing'
# Use carefully — when the && branch fails, || runs as if 'missing'.
# For complex logic, use 'if'.
# Guard patterns
require_root() { [[ $EUID -eq 0 ]] || { echo 'must be root' >&2; exit 1; }; }
require_command() { command -v "$1" >/dev/null || { echo "need $1" >&2; exit 1; }; }
# ===== 8) Combine with parameter expansion =====
mode="${1:-dry-run}"
case "$mode" in
dry-run) echo 'dry run only' ;;
apply) echo 'applying changes' ;;
*) echo 'usage: $0 dry-run|apply'; exit 2 ;;
esac
# ===== 9) Trapping errors =====
trap 'echo "ERROR on line $LINENO. cmd: $BASH_COMMAND" >&2' ERR
# ===== 10) Common pitfalls =====
# - Using = in [[ ]] (works) vs == (also works) — be consistent
# - Comparing numbers with == (string comparison; '5' != '05')
# Use -eq for numbers
# - Forgetting to quote: [[ $x = foo ]] fails if x is empty
# Always: [[ "$x" == 'foo' ]]
# - Globs without quotes leak to the test — quote the literal:
# [[ "$name" == a* ]] (right side NOT quoted -> glob match)
# [[ "$name" == 'a*' ]] (right side quoted -> literal 'a*')
# ===== 11) Bash booleans + return codes =====
# In bash, EXIT CODE 0 means SUCCESS (truth), non-zero is failure.
# Functions can be used directly as conditions.
is_dir() { [[ -d "$1" ]]; }
if is_dir /etc; then echo 'yes'; fi
# ===== 12) ShellCheck catches almost everything =====
shellcheck script.sh
Why it matters
Default to `[[ ... ]]` for tests; it does not word-split, supports glob + regex, and is safer than POSIX `[ ... ]`. Combined with `case` for multi-way branches and `(( ... ))` for arithmetic, you can write conditionals that read as prose without the quoting horrors of legacy shell.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
if [[ -f /etc/hosts ]]; then
echo "hosts exists"
fi
case "$1" in
start) start ;;
stop) stop ;;
esac
Try it Yourself »
Discussion
Loading…