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.

🐧 if / elif / else structure
#!/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
The semicolon before then is required when then is on the same line as if. Alternatively, put then on the next line and omit the semicolon.
if tests any command. You can use 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 [[.

[ ] — POSIX test (compatible)
# 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 keyword (recommended)
# 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]+$ ]]
⚠️ Always quote variables in [ ]. If $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 [[ ]].

OperatorMeaningExample
-eqEqual to[ "$a" -eq "$b" ]
-neNot equal to[ "$a" -ne 0 ]
-ltLess than[ "$a" -lt 10 ]
-leLess than or equal to[ "$a" -le 10 ]
-gtGreater than[ "$a" -gt 0 ]
-geGreater than or equal to[ "$a" -ge 1 ]
🐧 Numeric comparison in practice
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

OperatorMeaningNotes
= or ==Strings are equalUse = in [ ], either in [[ ]]. In [[ ]], the right side is treated as a glob pattern.
!=Strings are not equal 
<Lexicographically less thanIn [ ], must escape: \<. In [[ ]], use as-is.
>Lexicographically greater thanSame escaping caveat as <.
-zString is empty (zero length)[ -z "$var" ]
-nString is non-empty[ -n "$var" ]
=~String matches a regex[[ ]] only. Do not quote the pattern.
🐧 String comparison examples
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
When using =~, 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.

OperatorTrue if…
-e fileFile exists (any type)
-f fileFile exists and is a regular file
-d fileFile exists and is a directory
-L fileFile exists and is a symbolic link
-r fileFile exists and is readable by the current user
-w fileFile exists and is writable by the current user
-x fileFile exists and is executable by the current user
-s fileFile exists and has a size greater than zero
-z fileFile exists and has a size of zero
-b fileFile is a block device
-c fileFile is a character device
-p fileFile is a named pipe (FIFO)
f1 -nt f2f1 is newer than f2 (modification time)
f1 -ot f2f1 is older than f2
f1 -ef f2f1 and f2 refer to the same file (hard link or same inode)
🐧 File tests in a script
#!/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 [[ ]].

OperatorIn [ ]In [[ ]]Meaning
AND-a&&Both conditions must be true
OR-o||At least one condition must be true
NOT!!Negate the condition
🐧 Combining conditions
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.

🐧 Short-circuit command chaining
# 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"
Note the { } 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 ;;.

🐧 case syntax
#!/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.

🐧 Matching multiple values and using globs
#!/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 ;;&

🐧 Bash 4+ fall-through syntax
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.
Fall-through with ;& 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

Checking that an argument is a positive integer
#!/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

Guard clause pattern — fail early
#!/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

A reusable confirm() function
#!/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

SyntaxWhat it does
if cmd; then … fiRuns if cmd exits with 0
if [ expr ]; then … fiPOSIX test — quote all variables
if [[ expr ]]; then … fiBash test — glob + regex, safer with variables
if (( expr )); then … fiArithmetic test — C-style operators
-eq -ne -lt -le -gt -geNumeric comparisons (inside [ ] or [[ ]])
= != < > -z -nString comparisons
=~Regex match ([[ ]] only)
-e -f -d -r -w -x -s -LFile tests
&& || !Logical AND, OR, NOT inside [[ ]]
-a -o !Logical AND, OR, NOT inside [ ]
cmd1 && cmd2Run cmd2 only if cmd1 succeeds
cmd1 || cmd2Run cmd2 only if cmd1 fails
case "$var" in pat) … ;; esacMulti-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.

Exercise 1
Write a script called 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.
Hint: use nested if blocks with -e, -f, -d, -r, -w, -x. Guard against missing input with [[ -z "$1" ]].
Sample Solution
#!/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
Exercise 2
Write a script called 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.
Hint: use [[ "$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.
Sample Solution
#!/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
Exercise 3
Write a script called 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.
Hint: use ${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.
Sample Solution
#!/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
Exercise 4
Write a script called 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.
Hint: use -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.
Sample Solution
#!/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