Zsh Conditionals: if/else, File Tests & case Statements | Shell Scripting Tutorial #9
Video: Zsh Conditionals: if/else, File Tests & case Statements | Shell Scripting Tutorial #9 by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 9: Conditionals — if, [[ ]], File Tests, case
if [[ condition ]]; then ... fi. String tests==,!=,-z(empty),-n(non-empty). Numeric-eq -ne -lt -le -gt -ge. File tests-eexists,-ffile,-ddir,-rreadable. Logical&&||.case ... esacfor multi-way switch.
Conditionals make scripts decide. The Zsh syntax is verbose but readable once you know the operators.
if/then/fi
if [[ condition ]]; then
# commands
elif [[ other_condition ]]; then
# commands
else
# commands
fi
fi ends the block (if backwards). then always on the same line as if/elif after a ;, or on the next line.
if [[ -e file.txt ]]; then
echo "exists"
fi
String tests
name="Alice"
empty=""
[[ -n "$name" ]] # non-empty
[[ -z "$empty" ]] # zero length (empty)
[[ "$name" == "Alice" ]] # equal
[[ "$name" != "Bob" ]] # not equal
# Pattern match (glob)
[[ "report.txt" == *.txt ]] # true
# Regex match (Zsh extension)
[[ "abc123" =~ ^[a-z]+[0-9]+$ ]] # true; groups in ${match[1]}
Always quote variables in tests. Without quotes, an empty variable becomes nothing and changes the syntax:
# UNSAFE
if [[ $x == hello ]]; then ... # if $x is empty, becomes "[[ == hello ]]"
# SAFE
if [[ "$x" == hello ]]; then ...
Inside [[ ]], Zsh is more forgiving than Bash, but quote anyway for portability.
Numeric tests
age=25
limit=18
[[ $age -gt $limit ]] # greater than
[[ $age -ge $limit ]] # greater or equal
[[ $age -lt $limit ]] # less than
[[ $age -le $limit ]] # less or equal
[[ $age -eq $limit ]] # equal
[[ $age -ne $limit ]] # not equal
Mnemonic: gt, ge, lt, le, eq, ne — like Perl/Fortran.
For numeric comparison, you can also use (( )):
if (( age > limit )); then
echo "older"
fi
if (( count == 0 )); then
echo "empty"
fi
(( )) uses C-style operators (>, <, ==, !=, &&, ||). Cleaner for math-heavy logic.
File tests
target="${1:-/etc/hosts}"
[[ -e "$target" ]] # exists (file or directory)
[[ -f "$target" ]] # regular file
[[ -d "$target" ]] # directory
[[ -L "$target" ]] # symlink
[[ -r "$target" ]] # readable
[[ -w "$target" ]] # writable
[[ -x "$target" ]] # executable
[[ -s "$target" ]] # non-empty (size > 0)
[[ "$file1" -nt "$file2" ]] # newer than
[[ "$file1" -ot "$file2" ]] # older than
Negate with !:
[[ ! -e "$path" ]] # doesn't exist
[[ ! -d "$path" ]] # not a directory
Logical operators: && and ||
Inside [[ ]]:
if [[ -f "$file" && -r "$file" ]]; then
echo "readable file"
fi
if [[ -f "$path" || -d "$path" ]]; then
echo "exists"
fi
Outside [[ ]], && and || chain commands by exit status:
mkdir -p backup && cp -a src/ backup/
# (only copy if mkdir succeeded)
[[ -f config.yaml ]] || { echo "missing config" >&2; exit 1; }
# (error if config absent)
A grade checker
echo -n "Enter your score (0-100): "
read score
if [[ $score -ge 90 ]]; then
echo "Grade: A - Excellent!"
elif [[ $score -ge 80 ]]; then
echo "Grade: B - Good job!"
elif [[ $score -ge 70 ]]; then
echo "Grade: C - Not bad."
elif [[ $score -ge 60 ]]; then
echo "Grade: D - Needs work."
else
echo "Grade: F - Try again."
fi
elif chains check top-to-bottom; the first matching branch runs.
case: multi-way switch
For multi-way pattern matching, case is cleaner than nested if/elif:
case "$1" in
start)
echo "Starting..."
;;
stop)
echo "Stopping..."
;;
restart|reload) # multiple patterns separated by |
echo "Restarting..."
;;
status)
echo "Status..."
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
Each pattern uses ) to start its block, ;; to end. Patterns can be globs (*.txt, ???) or alternations (a|b|c). The *) at the end is the catch-all.
read -p "Color: " color
case "$color" in
red|orange|yellow) echo "warm" ;;
blue|green|purple) echo "cool" ;;
*) echo "neutral" ;;
esac
[[ ]] vs [ ] vs test
Three forms exist:
[[ condition ]]— Zsh/Bash extension. Modern. Use this.[ condition ]— POSIX. Older.=for string equality.test condition— same as[ ], just a different syntax.
[[ ]] is preferred:
- No word splitting on unquoted variables.
- Pattern matching with
==and!=. =~for regex.&&/||inside the brackets.- Cleaner overall.
For maximum portability (e.g., /bin/sh), use [ ]. Otherwise, always [[ ]].
Combining tests
# Check that a path exists, is a directory, and is readable
if [[ -d "$dir" && -r "$dir" ]]; then
process "$dir"
fi
# Multiple OR
if [[ "$mode" == "dev" || "$mode" == "test" ]]; then
echo "non-prod"
fi
# Negation
if [[ ! -f "$config" ]]; then
echo "config missing" >&2
exit 1
fi
Empty/unset distinction
unset x
y=""
[[ -z "${x:-}" ]] # true — x is unset
[[ -z "$y" ]] # true — y is empty
[[ -v x ]] # false — x is unset
[[ -v y ]] # true — y is set (even if empty)
-v distinguishes "set" from "non-empty." -z only checks length.
A real-world script
#!/usr/bin/env zsh
set -euo pipefail
CONFIG="${CONFIG:-./config.yaml}"
# Validate config exists and is readable
if [[ ! -f "$CONFIG" ]]; then
echo "Error: $CONFIG not found" >&2
exit 1
fi
if [[ ! -r "$CONFIG" ]]; then
echo "Error: $CONFIG not readable" >&2
exit 1
fi
# Determine action from first arg
ACTION="${1:-status}"
case "$ACTION" in
start)
[[ -f /tmp/myapp.pid ]] && { echo "Already running" >&2; exit 2; }
echo "Starting..."
;;
stop)
[[ -f /tmp/myapp.pid ]] || { echo "Not running" >&2; exit 2; }
echo "Stopping..."
rm /tmp/myapp.pid
;;
status)
if [[ -f /tmp/myapp.pid ]]; then
echo "Running"
else
echo "Stopped"
fi
;;
*)
echo "Usage: $0 {start|stop|status}" >&2
exit 1
;;
esac
Standard service-script shape: validate prereqs, dispatch on action.
Common stumbles
if [ $var = "value" ] with empty $var becomes [ = "value" ] — error. Always quote: [ "$var" = "value" ]. Or use [[ ]] (no word splitting).
== vs =. [[ ]] accepts both for string equality. [ ] requires =. Stick with == for [[ ]].
Numeric in [[ ]]. [[ "5" -gt "3" ]] works — [[ ]] does numeric comparison via -gt etc. But [[ "5" > "3" ]] does string comparison. For numbers, use -gt or (( )).
then placement. if [[ x ]] then errors. Need a ; or newline: if [[ x ]]; then.
Forgot fi. "syntax error: unexpected end of file." Every if needs a closing fi.
Glob in [[ ]] vs literal. [[ "$name" == *.txt ]] is a glob. To match a literal *, quote the right side: [[ "$name" == "*.txt" ]].
Regex with quotes. [[ "abc" =~ "[a-z]" ]] — the regex is quoted, so it's literal. Don't quote regex: [[ "abc" =~ [a-z] ]].
case patterns matching too greedily. case $x in 1) ... ;; *) ... ;; — * matches everything including the empty string. Order patterns from specific to general.
What's next
Lesson 10: loops. for, while, until, file processing.
Recap
if [[ cond ]]; then ... elif ... else ... fi. String: ==, !=, -z (empty), -n (non-empty). Numeric: -eq -ne -lt -le -gt -ge or (( )) with <>==. File: -e -f -d -L -r -w -x -s. Logical && || inside [[ ]] and outside (chains by exit code). case ... esac for switching on patterns. Always quote variables. Use [[ ]] over [ ].
Next lesson: loops.