Bash conditionals look simple until the first script breaks on an empty variable. The syntax is if CONDITION; then ...; elif OTHER; then ...; else ...; fi, but the condition itself runs in one of three test contexts ([ ], [[ ]], (( ))) and each behaves differently around quoting, word splitting, and operator precedence. Below is the full reference: every operator (numeric, string, file), the test-context comparison table, the case statement when elif chains get long, the short-circuit &&/|| shortcut and why it's subtly buggy, and the unquoted-variable pitfall that catches everyone exactly once.
How do I write an if-else in Bash?
A Bash if-else runs a block of commands when a test condition is true and a different block when it's false. The syntax is if CONDITION; then COMMANDS; else OTHER_COMMANDS; fi. Add elif CONDITION; then ... between if and else for additional branches. The condition can be any command (the exit status is the test), but most often you'll use one of three test contexts: [ EXPR ] (POSIX, max portability), [[ EXPR ]] (Bash builtin, richer string and pattern matching), or (( EXPR )) (arithmetic, no quoting needed). Numeric comparisons use -eq, -ne, -lt, -le, -gt, -ge inside [ ]; strings use =, !=, -z (empty), -n (non-empty); files use -f (regular file), -d (directory), -e (exists), -r/-w/-x (perms). Always quote variables in [ ] to avoid syntax errors on empty values. For known-list iteration, see Bash For Loops; for condition-driven repetition, see Bash While Loops.
Jump to:
- The four most common conditional patterns
- Test contexts:
[ ]vs[[ ]]vs(( )) - Numeric operators
- String operators
- File operators
- Combining tests with
&&,||, and! elifchains and thecasealternative- The
&&/||ternary shortcut and its trap - Common pitfalls
- What to do next
- FAQ
The four most common conditional patterns
# 1. Simple if-else
if [ -f /etc/hosts ]; then
echo "File exists"
else
echo "Missing"
fi
# 2. if-elif-else chain
if (( score >= 90 )); then
echo "A"
elif (( score >= 80 )); then
echo "B"
else
echo "C or below"
fi
# 3. Test string equality with [[ ]]
if [[ "$env" == "prod" ]]; then
echo "Production deploy"
fi
# 4. Negate a test with !
if ! command -v jq > /dev/null; then
echo "jq is not installed" >&2
exit 1
fiThese four shapes cover ~90% of real-world conditionals. The rest of this page covers the operators, the test-context choice, and the pitfalls that turn working scripts into scripts that misbehave on edge cases.
Test contexts: [ ] vs [[ ]] vs (( ))
Bash gives you three test contexts. Knowing which to use when is the difference between robust conditionals and scripts that break on the first empty variable.
| Context | Use case | Example |
|---|---|---|
[ ] (POSIX test) | Maximum portability; works in sh as well as Bash | if [ "$x" -lt 10 ]; then ... |
[[ ]] (Bash builtin) | Rich string ops, glob and regex matching, no word splitting | if [[ "$file" == *.log ]]; then ... |
(( )) (arithmetic) | Numeric comparison, no quoting, C-style operators | if (( x < 10 )); then ... |
Practical guidance:
- Numeric: prefer
(( )). No quoting, supports++,--,+=,-=,<=,>=,<,>directly. - String equality, pattern, regex: prefer
[[ ]]. Supports==,!=,=~(regex),<,>(lexicographic), and globs on the right side. - POSIX
shcompatibility required: stick to[ ]and quote every variable.
The single most common bug is using [ ] with an unquoted variable that happens to be empty: [ $x -lt 10 ] becomes [ -lt 10 ] and Bash errors out. [[ ]] and (( )) don't have this problem.
Numeric operators
Inside [ ] (or [[ ]] when you want POSIX-style), use the dash-prefixed operators:
| Operator | Meaning | Example |
|---|---|---|
-eq | equal | [ "$x" -eq 10 ] |
-ne | not equal | [ "$x" -ne 0 ] |
-lt | less than | [ "$x" -lt 100 ] |
-le | less than or equal | [ "$x" -le 100 ] |
-gt | greater than | [ "$x" -gt 0 ] |
-ge | greater than or equal | [ "$x" -ge 18 ] |
Inside (( )), use C-style operators without quoting:
if (( count == 0 )); then
echo "Empty"
fi
if (( score >= 80 && score < 90 )); then
echo "B grade"
fi
# Arithmetic AND a side-effect increment in one expression
if (( attempts++ < 3 )); then
echo "Try again, attempt $attempts"
fi(( )) returns exit status 0 if the result is non-zero (truthy in arithmetic) and 1 if the result is zero (falsy). That's the opposite of how it reads, but it matches C semantics.
String operators
| Operator | Meaning | Example |
|---|---|---|
= or == | equal | [ "$x" = "yes" ] |
!= | not equal | [[ "$x" != "no" ]] |
-z | empty (zero length) | [ -z "$x" ] |
-n | non-empty | [ -n "$x" ] |
< | lexicographically less | [[ "$a" < "$b" ]] |
> | lexicographically greater | [[ "$a" > "$b" ]] |
=~ | regex match | [[ "$x" =~ ^[0-9]+$ ]] |
Two things to know:
- Inside
[ ], use=(POSIX) rather than==(Bash-only). Inside[[ ]], both work. <and>are redirection operators in[ ]. They only do string comparison inside[[ ]].
A common idiom: check if a variable is set AND non-empty before using it.
if [ -z "$DEPLOY_TOKEN" ]; then
echo "DEPLOY_TOKEN is not set" >&2
exit 1
fiRegex matching (Bash 4+) is one of the strongest reasons to use [[ ]]:
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "Looks like an email"
fiThe regex goes on the right side and must NOT be quoted — quotes turn it into a literal string match.
File operators
The full file-test set is rich. The ones you'll actually use:
| Operator | True when... |
|---|---|
-f FILE | FILE exists and is a regular file |
-d FILE | FILE exists and is a directory |
-e FILE | FILE exists (any type) |
-r FILE | FILE is readable by the current user |
-w FILE | FILE is writable by the current user |
-x FILE | FILE is executable by the current user |
-s FILE | FILE exists and is non-empty |
-L FILE | FILE is a symbolic link |
Examples:
# Guard before sourcing config
if [ -f "$HOME/.deployrc" ]; then
source "$HOME/.deployrc"
fi
# Bail out if the backup directory doesn't exist
if [ ! -d "$BACKUP_DIR" ]; then
echo "Backup dir missing: $BACKUP_DIR" >&2
exit 1
fi
# Skip processing zero-byte log files
for log in *.log; do
[ -s "$log" ] || continue
process "$log"
doneSame pattern shows up in the How to Export All MySQL Databases backup script (file-size sanity check before rotating), and in the How to Increase Google Cloud VM Disk Size Without Rebooting walkthrough (waiting for the device file to appear).
Combining tests with &&, ||, and !
The modern way to combine tests is with && (logical AND) and || (logical OR) BETWEEN brackets, not inside them.
# Both must be true
if [ -f config.yml ] && [ -r config.yml ]; then
echo "Config exists and is readable"
fi
# Either is enough
if [ -z "$VAR1" ] || [ -z "$VAR2" ]; then
echo "At least one var is empty"
fi
# Negation
if ! [ -d /var/log/myapp ]; then
mkdir -p /var/log/myapp
fiInside [[ ]], you can use && and || directly — no need to split the brackets:
if [[ -f config.yml && -r config.yml ]]; then
echo "Config exists and is readable"
fiThe legacy -a (AND) and -o (OR) inside a single [ ] are deprecated:
# Don't write this — POSIX itself flags -a/-o as obsolete
if [ "$x" -eq 1 -a "$y" -eq 2 ]; then ...
# Write this instead
if [ "$x" -eq 1 ] && [ "$y" -eq 2 ]; then ...shellcheck will flag the deprecated form. Stick to the split-bracket idiom.
elif chains and the case alternative
Three branches is fine for elif. Five branches is a smell. Beyond that, case reads better.
# elif chain — works but gets noisy fast
if [ "$env" = "dev" ]; then
echo "Development"
elif [ "$env" = "staging" ]; then
echo "Staging"
elif [ "$env" = "prod" ]; then
echo "Production"
else
echo "Unknown env: $env" >&2
exit 1
fi
# case — the right tool for multi-branch on one variable
case "$env" in
dev) echo "Development" ;;
staging) echo "Staging" ;;
prod) echo "Production" ;;
*) echo "Unknown env: $env" >&2; exit 1 ;;
esaccase matches glob patterns, not just literal strings:
case "$file" in
*.log|*.txt) echo "Text-ish: $file" ;;
*.gz|*.tar) echo "Archive: $file" ;;
*) echo "Other: $file" ;;
esacThe ;; terminates each branch. *) is the default case (matches anything). Pipe | joins patterns within one branch. For a deep dive on the comparable iteration pattern, see Bash Arrays and how case pairs naturally with array dispatch.
The &&/|| ternary shortcut and its trap
Bash supports a one-line "if-then-else" via && and ||:
# Looks like a ternary: condition && then-branch || else-branch
[ -f config.yml ] && echo "exists" || echo "missing"This works in most cases, but it has a subtle bug: if the then-branch itself fails (returns non-zero), the else-branch ALSO runs.
# BAD — if echo "exists" somehow fails (e.g., disk full), you get BOTH messages
[ -f config.yml ] && echo "exists" || echo "missing"For two-message-or-one logic that you actually trust, use a real if:
if [ -f config.yml ]; then
echo "exists"
else
echo "missing"
fiThe short-circuit form is fine when the then-branch is a simple variable assignment or a guaranteed-zero-exit command (true, :):
# Safe — assignment never fails
[ -f config.yml ] && config_exists=1 || config_exists=0
# Safe — short-circuit guards a side effect
mkdir -p /tmp/work || { echo "Cannot create work dir" >&2; exit 1; }The rule of thumb: use &&/|| for guard clauses, use if/else for branching logic.
Common pitfalls
1. Unquoted variables in [ ]. [ $x -lt 10 ] becomes [ -lt 10 ] and errors out when $x is empty. Always quote: [ "$x" -lt 10 ]. Better: switch to [[ ]] or (( )) which don't have this problem.
2. Using == in [ ]. POSIX test only defines =. [ "$x" == "y" ] works in Bash but breaks in dash/sh. Use = in [ ]; use == only in [[ ]].
3. Mixing numeric and string operators. [ "$x" -lt "10" ] works; [ "$x" < "10" ] is a STRING comparison (and also a redirect attempt). Use -lt in [ ] and < only inside (( )) or [[ ]].
4. Quoting the regex in [[ =~ ]]. [[ "$x" =~ "^[0-9]+$" ]] matches the literal string ^[0-9]+$, not a regex. The regex on the right side of =~ must be unquoted.
5. Forgetting then or fi. Bash gives confusing "syntax error near unexpected token" messages for missing then or fi. Trace the indent.
6. else if instead of elif. Bash uses elif. else if is two separate constructs and creates an unterminated outer if.
7. The &&/|| ternary trap. [ X ] && A || B runs B if A fails. Use if/else for two-message logic.
8. Comparing numbers with the wrong context. if [ "$count" > 10 ] is a redirection, not a comparison. Use [ "$count" -gt 10 ] or (( count > 10 )).
9. Negation order with !. ! [ -f file ] works; [ ! -f file ] also works. Both are fine; pick one and stay consistent.
Debugging Bash conditionals
# Trace every command and its arguments
bash -x script.sh
# Trace one section
set -x
if [ -f config.yml ]; then
source config.yml
fi
set +x
# Static analysis — catches every pitfall above
shellcheck script.shshellcheck flags unquoted variables, deprecated -a/-o, == in [ ], missing quotes around regex, and most other conditional bugs. Install with brew install shellcheck or apt install shellcheck.
For the loop counterparts that share the same test-context rules, see Bash For Loops and Bash While Loops. For passing branching decisions into reusable code, see Bash Functions.
What to do next
- Bash For Loops — known-list iteration. Same operator rules apply inside the loop body.
- Bash While Loops — condition-driven iteration. The
whilecondition uses the same[ ]/[[ ]]/(( ))contexts. - Bash Arrays — combine arrays with
casefor clean dispatch tables. - Bash Functions — wrap conditional logic in a function with
localvariables and exit-code returns. - How to Export and Import PuTTY Settings — pairs Bash conditionals with
regeditfor cross-platform automation. - How to Export All MySQL Databases — backup script with file-size and exit-code guards.
- How to Increase Google Cloud VM Disk Size Without Rebooting — uses file-existence conditionals to wait for the resized device.
- External: the Bash Reference Manual: Conditional Constructs is the authoritative spec.

![Bash if, else, elif: Syntax, Test Operators, and Examples (2026) Bash conditionals reference: if/elif/else syntax, [ ] vs [[ ]] vs (( )) test contexts, numeric, string, and file operators, the case statement, and the unquoted-variable pitfall.](https://techearl.com/cdn-cgi/image/width=1536,quality=85,format=auto/https://images.techearl.com/bash-if-else/bash-if-else.jpg?v=2026-01-08T11%3A24%3A00Z)


![Bash while loop reference: read files with while IFS= read -r, retry-with-backoff, wait-for-service polling, the subshell-scoping bug fix, the until and select siblings, plus [[ ]] vs [ ] vs (( )) test contexts.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=80/https://images.techearl.com/bash-while-loop/bash-while-loop.jpg?v=2026-04-25T13%3A18%3A00Z)
