Conditional Statements
🔀 Topic 5 — Conditional Statements
Conditionals let a script make decisions — running different code depending on whether a condition is true or false. This chapter covers if/elif/else, the two test syntaxes [ ] and [[ ]] with their full set of operators, file tests, logical connectives, the case statement for multi-branch matching, and the compact && / || shorthand. By the end you will be able to write scripts that respond intelligently to their environment and input.
1 — How if Works
In bash, if does not test a boolean value — it runs a command and checks its exit status. If the exit status is 0 (success), the condition is true; any non-zero exit status is false. The test commands [ and [[ are simply commands that return 0 or non-zero based on a comparison.
#!/bin/bash
score=72
if [ "$score" -ge 90 ]; then
echo "Grade: A"
elif [ "$score" -ge 70 ]; then
echo "Grade: B"
elif [ "$score" -ge 50 ]; then
echo "Grade: C"
else
echo "Grade: F"
fi
Grade: B
then is required when then is on the same line as if. Alternatively, put then on the next line and omit the semicolon.if grep -q "error" logfile; then — if grep finds a match (exit 0), the block runs. This means every command in bash is potentially a condition. The [ and [[ commands are just the most common ones used with if.
2 — [ ] vs [[ ]] — Which to Use
[ is a traditional POSIX command (also called test) — it is available in every shell. [[ is a bash built-in keyword that extends [ with extra features and fewer surprises. For bash scripts, prefer [[.
# Works in any sh-compatible shell
# Variables MUST be quoted
[ "$name" = "Philip" ]
# Logical AND uses -a
[ "$a" -gt 0 -a "$a" -lt 10 ]
# No regex or glob support
# No &&, || inside brackets
# Bash only — not POSIX sh
# Unquoted variables are safe
[[ $name == "Philip" ]]
# Logical AND uses &&
[[ $a -gt 0 && $a -lt 10 ]]
# Glob matching with ==
[[ $file == *.txt ]]
# Regex matching with =~
[[ $input =~ ^[0-9]+$ ]]
$var is empty or contains spaces, an unquoted [ $var = "x" ] will either throw a syntax error or give wrong results. In [[ ]], word-splitting does not apply so unquoted variables are safe — though quoting is still good practice.
3 — Numeric Comparisons
For comparing integers, bash uses flag-based operators (not the < / > symbols, which mean redirection in this context). These work identically inside both [ ] and [[ ]].
| Operator | Meaning | Example |
|---|---|---|
-eq | Equal to | [ "$a" -eq "$b" ] |
-ne | Not equal to | [ "$a" -ne 0 ] |
-lt | Less than | [ "$a" -lt 10 ] |
-le | Less than or equal to | [ "$a" -le 10 ] |
-gt | Greater than | [ "$a" -gt 0 ] |
-ge | Greater than or equal to | [ "$a" -ge 1 ] |
age=17
if [[ $age -lt 18 ]]; then
echo "You must be 18 or older."
elif [[ $age -ge 18 && $age -lt 65 ]]; then
echo "Standard admission."
else
echo "Senior discount applies."
fi
You must be 18 or older.
# You can also use (( )) for numeric conditions — reads more naturally
if (( age >= 18 && age < 65 )); then
echo "Standard admission."
fi
(( )) uses C-style comparison symbols (> < == !=) and does not need $ on variable names. It is often the most readable choice for pure numeric tests.4 — String Comparisons
| Operator | Meaning | Notes |
|---|---|---|
= or == | Strings are equal | Use = in [ ], either in [[ ]]. In [[ ]], the right side is treated as a glob pattern. |
!= | Strings are not equal | |
< | Lexicographically less than | In [ ], must escape: \<. In [[ ]], use as-is. |
> | Lexicographically greater than | Same escaping caveat as <. |
-z | String is empty (zero length) | [ -z "$var" ] |
-n | String is non-empty | [ -n "$var" ] |
=~ | String matches a regex | [[ ]] only. Do not quote the pattern. |
name="Philip"
# Equality
if [[ "$name" == "Philip" ]]; then echo "Hello, Philip!"; fi
Hello, Philip!
# Glob matching — right side is a pattern, not quoted
if [[ "$name" == Ph* ]]; then echo "Starts with Ph"; fi
Starts with Ph
# Regex matching with =~ (POSIX extended regex)
email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email format"
fi
Valid email format
# Empty / non-empty checks
input=""
if [[ -z "$input" ]]; then echo "Input is empty"; fi
Input is empty
# Lexicographic ordering
if [[ "apple" < "banana" ]]; then echo "apple comes first"; fi
apple comes first
=~, capture groups are stored in the BASH_REMATCH array: ${BASH_REMATCH[0]} is the full match, ${BASH_REMATCH[1]} is the first group, etc.5 — File Test Operators
File tests check properties of files and directories — whether they exist, what type they are, and what permissions they have. These are some of the most frequently used tests in real-world scripts.
| Operator | True if… |
|---|---|
-e file | File exists (any type) |
-f file | File exists and is a regular file |
-d file | File exists and is a directory |
-L file | File exists and is a symbolic link |
-r file | File exists and is readable by the current user |
-w file | File exists and is writable by the current user |
-x file | File exists and is executable by the current user |
-s file | File exists and has a size greater than zero |
-z file | File exists and has a size of zero |
-b file | File is a block device |
-c file | File is a character device |
-p file | File is a named pipe (FIFO) |
f1 -nt f2 | f1 is newer than f2 (modification time) |
f1 -ot f2 | f1 is older than f2 |
f1 -ef f2 | f1 and f2 refer to the same file (hard link or same inode) |
#!/bin/bash
path="$1"
if [[ -z "$path" ]]; then
echo "Usage: $0 <path>" >&2
exit 1
fi
if [[ ! -e "$path" ]]; then
echo "'$path' does not exist."
elif [[ -d "$path" ]]; then
echo "'$path' is a directory."
elif [[ -f "$path" ]]; then
if [[ -r "$path" && -w "$path" ]]; then
echo "'$path' is a readable and writable file."
elif [[ -r "$path" ]]; then
echo "'$path' is readable but not writable."
else
echo "'$path' exists but is not readable."
fi
else
echo "'$path' exists but is not a regular file or directory."
fi
6 — Logical Operators
Logical operators let you combine multiple conditions into a single test. The syntax differs slightly between [ ] and [[ ]].
| Operator | In [ ] | In [[ ]] | Meaning |
|---|---|---|---|
| AND | -a | && | Both conditions must be true |
| OR | -o | || | At least one condition must be true |
| NOT | ! | ! | Negate the condition |
age=25
member="yes"
# AND — both must be true
if [[ $age -ge 18 && "$member" == "yes" ]]; then
echo "Access granted."
fi
Access granted.
# OR — either condition is enough
role="admin"
if [[ "$role" == "admin" || "$role" == "superuser" ]]; then
echo "Elevated privileges."
fi
Elevated privileges.
# NOT — negate a condition
file="config.cfg"
if [[ ! -f "$file" ]]; then
echo "Config file missing — creating default."
touch "$file"
fi
# Combining three or more conditions
if [[ $age -ge 18 && $age -lt 65 && "$member" == "yes" ]]; then
echo "Full member benefits apply."
fi
Chaining with && and || Outside Brackets
The && and || operators can also be used outside brackets to chain commands — running the second command only if the first succeeded or failed.
# cmd1 && cmd2 — run cmd2 only if cmd1 succeeds (exit 0)
mkdir -p /tmp/mydir && echo "Directory created."
# cmd1 || cmd2 — run cmd2 only if cmd1 FAILS (non-zero exit)
cd /nonexistent || echo "ERROR: directory not found." >&2
# Common pattern: exit on failure
cp source.txt dest.txt || { echo "Copy failed" >&2; exit 1; }
# Guard clause — ensure a directory exists before writing
[[ -d "$output_dir" ]] || mkdir -p "$output_dir"
{ } grouping in the third example — without the braces, only echo would be the "or" branch; exit 1 would always run. Braces group multiple commands into one for ||.7 — The case Statement
When you need to match a value against many possible patterns, a case statement is far cleaner than a long chain of elif blocks. Each branch uses glob-style patterns and ends with ;;.
#!/bin/bash
case "$1" in
start)
echo "Starting the service..."
;;
stop)
echo "Stopping the service..."
;;
restart)
echo "Restarting the service..."
;;
status)
echo "Checking status..."
;;
*)
echo "Usage: $0 {start|stop|restart|status}" >&2
exit 1
;;
esac
Multiple Patterns per Branch
Separate patterns with | to match several values in one branch.
#!/bin/bash
read -r -p "Enter a file name: " fname
case "${fname,,}" in # ${fname,,} lowercases input first
*.jpg | *.jpeg | *.png | *.gif | *.webp)
echo "Image file detected."
;;
*.mp4 | *.mkv | *.avi | *.mov)
echo "Video file detected."
;;
*.sh | *.bash)
echo "Shell script detected."
;;
*.txt | *.md | *.csv)
echo "Text file detected."
;;
"")
echo "No filename entered."
;;
*)
echo "Unknown file type."
;;
esac
Fall-through with ;& and ;;&
level="gold"
case "$level" in
platinum)
echo "Platinum perk: lounge access."
;& # ;& falls through to the NEXT branch unconditionally
gold)
echo "Gold perk: priority boarding."
;&
silver)
echo "Silver perk: extra baggage."
;;
*)
echo "Standard tier."
;;
esac
Gold perk: priority boarding.
Silver perk: extra baggage.
# ;; stops. ;& continues to next. ;;& re-tests remaining patterns.
;& is bash 4+ only and is rarely needed. The more common ;;; is the standard terminator — it stops after the matching branch.8 — Practical Patterns
Validate a numeric argument
#!/bin/bash
input="$1"
if [[ ! "$input" =~ ^[0-9]+$ ]]; then
echo "ERROR: '$input' is not a positive integer." >&2
exit 1
fi
echo "Valid number: $input"
Require a file to exist before proceeding
#!/bin/bash
config="$HOME/.myapp/config"
[[ -f "$config" ]] || { echo "Config not found: $config" >&2; exit 1; }
[[ -r "$config" ]] || { echo "Config not readable." >&2; exit 1; }
# Only reaches here if both tests passed
echo "Loading config from $config..."
Interactive yes/no prompt
#!/bin/bash
confirm() {
read -r -p "$1 [y/N]: " response
case "${response,,}" in
y | yes) return 0 ;; # return 0 = true
*) return 1 ;; # return 1 = false
esac
}
if confirm "Delete all log files?"; then
rm -f /var/log/myapp/*.log
echo "Logs deleted."
else
echo "Cancelled."
fi
9 — Quick Reference
| Syntax | What it does |
|---|---|
if cmd; then … fi | Runs if cmd exits with 0 |
if [ expr ]; then … fi | POSIX test — quote all variables |
if [[ expr ]]; then … fi | Bash test — glob + regex, safer with variables |
if (( expr )); then … fi | Arithmetic test — C-style operators |
-eq -ne -lt -le -gt -ge | Numeric comparisons (inside [ ] or [[ ]]) |
= != < > -z -n | String comparisons |
=~ | Regex match ([[ ]] only) |
-e -f -d -r -w -x -s -L | File tests |
&& || ! | Logical AND, OR, NOT inside [[ ]] |
-a -o ! | Logical AND, OR, NOT inside [ ] |
cmd1 && cmd2 | Run cmd2 only if cmd1 succeeds |
cmd1 || cmd2 | Run cmd2 only if cmd1 fails |
case "$var" in pat) … ;; esac | Multi-branch pattern matching |
pat1 | pat2) | Match either pattern in a case branch |
${BASH_REMATCH[n]} | Regex capture groups from =~ match |
✏️ Exercises
Apply what you have learned in this chapter. Try each exercise yourself before looking at the sample solution.
filecheck.sh that accepts a file path as an argument and reports: whether the path exists; if it does, whether it is a file or directory; and if it is a file, whether it is readable, writable, and/or executable. If no argument is given, print a usage message to stderr and exit with code 1.if blocks with -e, -f, -d, -r, -w, -x. Guard against missing input with [[ -z "$1" ]].#!/bin/bash
# filecheck.sh
if [[ -z "$1" ]]; then
echo "Usage: $0 <path>" >&2
exit 1
fi
path="$1"
if [[ ! -e "$path" ]]; then
echo "'$path' does not exist."
exit 0
fi
if [[ -d "$path" ]]; then
echo "'$path' is a directory."
elif [[ -f "$path" ]]; then
echo "'$path' is a regular file."
[[ -r "$path" ]] && echo " ✔ Readable"
[[ -w "$path" ]] && echo " ✔ Writable"
[[ -x "$path" ]] && echo " ✔ Executable"
[[ ! -r "$path" ]] && echo " ✘ Not readable"
[[ ! -w "$path" ]] && echo " ✘ Not writable"
[[ ! -x "$path" ]] && echo " ✘ Not executable"
else
echo "'$path' exists but is not a regular file or directory."
fi
validate.sh that prompts the user for an email address and a port number, validates both using =~ regex, and prints a clear pass/fail result for each. A valid port is a number between 1 and 65535.[[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] for email. For port, first check it is all digits with =~ ^[0-9]+$, then use (( port >= 1 && port <= 65535 )) for range.#!/bin/bash
# validate.sh
read -r -p "Email address: " email
read -r -p "Port number : " port
# Email check
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Email : ✔ Valid"
else
echo "Email : ✘ Invalid"
fi
# Port check — must be numeric AND in range
if [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); then
echo "Port : ✔ Valid ($port)"
else
echo "Port : ✘ Invalid (must be 1–65535)"
fi
daytype.sh that accepts a day name as an argument (e.g. Monday) and uses a case statement to print whether it is a weekday, weekend, or an unrecognised input. The check should be case-insensitive, so saturday, Saturday, and SATURDAY all give the same result.${1,,} to convert the argument to lowercase before the case statement, then match against the lowercase day names. Use | to group Monday–Friday in one branch.#!/bin/bash
# daytype.sh
if [[ -z "$1" ]]; then
echo "Usage: $0 <day name>" >&2; exit 1
fi
case "${1,,}" in
monday | tuesday | wednesday | thursday | friday)
echo "'$1' is a weekday."
;;
saturday | sunday)
echo "'$1' is a weekend day."
;;
*)
echo "'$1' is not a recognised day name."
exit 1
;;
esac
backup_check.sh that checks whether a backup directory (passed as an argument) exists and is writable, whether the directory contains any .tar.gz files, and whether the most recently modified .tar.gz file is newer than a file called last_backup.txt in the same directory. Print a clear status report for each check.-d and -w for the directory; use ls *.tar.gz 2>/dev/null with $? to check for archives; use -nt to compare modification times between files.#!/bin/bash
# backup_check.sh
dir="${1:?Usage: $0 <backup-dir>}"
# 1. Check directory exists and is writable
if [[ -d "$dir" && -w "$dir" ]]; then
echo "[✔] Directory exists and is writable."
elif [[ -d "$dir" ]]; then
echo "[✘] Directory exists but is NOT writable."
exit 1
else
echo "[✘] Directory '$dir' does not exist."
exit 1
fi
# 2. Check for .tar.gz files
latest=$(ls -t "$dir"/*.tar.gz 2>/dev/null | head -1)
if [[ -z "$latest" ]]; then
echo "[✘] No .tar.gz files found in '$dir'."
exit 0
else
echo "[✔] Latest archive: $(basename "$latest")"
fi
# 3. Check if latest archive is newer than last_backup.txt
marker="$dir/last_backup.txt"
if [[ ! -f "$marker" ]]; then
echo "[?] No last_backup.txt found — cannot compare timestamps."
elif [[ "$latest" -nt "$marker" ]]; then
echo "[✔] Backup is up to date (archive newer than marker)."
else
echo "[✘] Backup may be stale (archive older than marker)."
fi