Control Flow¶
if/elif/else and case statements are what turn a sequence of commands into a script that makes decisions — the foundation of any real automation.
Learning Objectives¶
- Write
if/elif/elsestatements using[](<#>)and(( )) - Use file and string test operators correctly
- Write
casestatements for multi-branch logic - Understand exit codes and how they drive conditional logic
Exit Codes¶
Every command returns an exit code: 0 means success, non-zero means failure. This is the foundation of all conditional logic in bash.
The rule: 0 = true, non-zero = false
In bash conditions, 0 is treated as true and any non-zero value as false. This is the opposite of most other languages. It makes sense because 0 means "no error."
if Statements¶
if [[ condition ]]; then
# commands if true
elif [[ other_condition ]]; then
# commands if elif true
else
# commands if none matched
fi
File Tests¶
if [-f "/etc/passwd"](<../-f "/etc/passwd".md>); then
echo "File exists"
fi
if [-d "$HOME/scripts"](<../-d "$HOME/scripts".md>); then
echo "Directory exists"
fi
if [! -e "/tmp/lockfile"](<../! -e "/tmp/lockfile".md>); then
echo "Lock file does not exist"
fi
Common file test operators:
| Operator | True if |
|---|---|
-f file |
file exists and is a regular file |
-d dir |
directory exists |
-e path |
path exists (any type) |
-r file |
file exists and is readable |
-w file |
file exists and is writable |
-x file |
file exists and is executable |
-s file |
file exists and has size > 0 |
-L file |
file is a symbolic link |
String Tests¶
name="Nikhil"
if [[ -z "$name" ]]; then
echo "Name is empty"
elif [[ -n "$name" ]]; then
echo "Name is: $name"
fi
if [[ "$name" == "Nikhil" ]]; then
echo "Hello, Nikhil"
fi
if [[ "$name" != "root" ]]; then
echo "Not running as root"
fi
# Pattern matching with ==
if [[ "$filename" == *.log ]]; then
echo "This is a log file"
fi
Numeric Tests¶
Use (( )) for arithmetic comparisons — it is cleaner than [](<#>) for numbers:
count=42
if (( count > 10 )); then
echo "More than 10"
fi
if (( count >= 40 && count <= 50 )); then
echo "Between 40 and 50"
fi
Or use -eq, -ne, -lt, -le, -gt, -ge inside [](<#>):
[](<#>) vs [ ] vs (( ))
Use [](<#>) for string and file tests — it is a bash keyword, safer, and supports &&, ||, and pattern matching. Use (( )) for arithmetic. Avoid [ ] (POSIX test) in bash scripts — it has subtler quoting rules and fewer features.
Combining Conditions¶
if [[ -f "$file" && -r "$file" ]]; then
echo "File exists and is readable"
fi
if [[ -z "$1" || "$1" == "--help" ]]; then
echo "Usage: $0 <filename>"
exit 0
fi
case Statements¶
case is cleaner than a long if/elif chain when you are matching a single value against multiple patterns:
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
--help|-h)
echo "Usage: $0 {start|stop|restart}"
;;
*)
echo "Unknown command: $1" >&2
exit 1
;;
esac
# case with glob patterns
case "$filename" in
*.jpg|*.jpeg|*.png)
echo "Image file"
;;
*.mp3|*.wav|*.flac)
echo "Audio file"
;;
*.sh)
echo "Shell script"
;;
*)
echo "Unknown file type"
;;
esac
Practical Pattern: Argument Checking¶
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <directory>" >&2
exit 1
fi
DIR="$1"
if [[ ! -d "$DIR" ]]; then
echo "Error: '$DIR' is not a directory" >&2
exit 1
fi
echo "Processing: $DIR"
Common Mistakes¶
Missing quotes around variables in tests
[[ $name == "Nikhil" ]] works because [](<#>) does not word-split. But [ $name == "Nikhil" ] fails if $name is empty or contains spaces. Always quote variable references even inside [](<#>) — it is a good habit.
Using = vs == in [](<#>)
Both = and == work for string equality inside [](<#>). Use == for clarity. Do not use == inside [ ] — there, you must use =.
Practice Exercises¶
Warm-Up (run and observe)¶
- Run
[[ "hello" == "hello" ]]; echo $?. Then try[[ "hello" == "world" ]]; echo $?. What exit codes do you see? - Run
[-f /etc/passwd](<../-f /etc/passwd.md>); echo $?and[-f /nonexistent](<../-f /nonexistent.md>); echo $?. What do the results mean? - Write a one-liner that prints "root" if
$USERequals "root" and "not root" otherwise.
Main (write a short script)¶
Create ~/scripts/file_type.sh:
#!/usr/bin/env bash
set -euo pipefail
FILE="${1:?Usage: $0 <path>}"
if [[ ! -e "$FILE" ]]; then
echo "Error: '$FILE' does not exist" >&2
exit 1
fi
if [[ -d "$FILE" ]]; then
echo "'$FILE' is a directory"
echo "Contains $(ls "$FILE" | wc -l) items"
elif [[ -L "$FILE" ]]; then
echo "'$FILE' is a symlink -> $(readlink "$FILE")"
elif [[ -f "$FILE" ]]; then
echo "'$FILE' is a regular file"
echo "Size: $(du -sh "$FILE" | cut -f1)"
echo "Lines: $(wc -l < "$FILE")"
fi
Stretch¶
- Write a script that checks if a given port is in your
/etc/servicesfile and prints its protocol. - Rewrite the
file_type.shusing acasestatement instead ofif/elif. - What happens if you write
if [ -z $unset_var ]whenunset_varis not defined? How does adding quotes fix it? How doesset -uchange the behavior?
Interview Questions¶
- What exit code does a successful command return, and why is 0 "true" in bash?
Show answer
A successful command returns 0. In bash, if command runs the then block if the command exits with 0 — this is "true." Non-zero means failure ("false"). This is the opposite of C, Python, etc. It makes sense because 0 means "no error" — there is only one way to succeed but many ways to fail (different non-zero error codes).
- What is the difference between
[](<#>)and[ ]?
Show answer
[](<#>) is a bash keyword — it does not perform word-splitting or glob expansion on variable values, supports && and || directly, allows == with glob patterns, and supports regex matching with =~. [ ] is the POSIX test command — it is more portable but has more quoting edge cases. Use [](<#>) in bash scripts.
- How do you check if two strings are equal in bash?
Show answer
[[ "$str1" == "$str2" ]] for exact equality. Note: == inside [](<#>) performs glob pattern matching on the right side (unquoted). [[ "$filename" == *.log ]] checks if filename ends in .log. For literal equality when the right side might contain glob characters, quote it: [[ "$str" == "*.log" ]].