Stage 2 · Tools
Bash Fundamentals
Control Flow
if/elif/else, case, for/while/until loops — controlling execution paths in Bash.
if/elif/else
The if statement in Bash is a command, not a keyword. It evaluates the exit code of a command or test expression. Zero means true, non-zero means false.
if [[ -f /etc/hosts ]]; then
echo "Hosts file exists"
elif [[ -f /etc/hosts.bak ]]; then
echo "Backup hosts file exists"
else
echo "No hosts file found"
fiThe [[ ]] syntax is Bash-specific and safer than [ ]. It handles empty variables, globbing, and regex matching correctly without requiring quotes.
[[ ]] is a Bash keyword that handles unquoted variables safely and supports regex with =~. [ ] is the POSIX test command that requires quoting and can break on edge cases. Use [[ ]] in Bash scripts.
Test Commands
The test command (and its shorthand [[ ]) checks conditions about files, strings, and numbers. These are the building blocks of conditionals.
| Test | Meaning |
|---|---|
| -f FILE | File exists and is a regular file |
| -d DIR | Directory exists |
| -e PATH | Path exists (file or directory) |
| -r FILE | File is readable |
| -w FILE | File is writable |
| -x FILE | File is executable |
| -s FILE | File exists and is not empty |
| -L FILE | Path is a symbolic link |
NAME=""
# String tests
[[ -z "$NAME" ]] && echo "Name is empty"
[[ -n "$NAME" ]] && echo "Name is not empty"
[[ "$NAME" == "Alice" ]] && echo "Name is Alice"
# Numeric tests
A=10
B=20
[[ $A -eq $B ]] && echo "Equal"
[[ $A -lt $B ]] && echo "A is less than B"
[[ $A -gt $B ]] && echo "A is greater than B"
[[ $A -ne $B ]] && echo "Not equal"Use -z to test for empty strings and -n for non-empty. For numeric comparison, use -eq, -ne, -lt, -le, -gt, -ge inside [[ ]]. For string comparison, use ==, !=, <, >.
case Statements
The case statement is Bash's pattern-matching alternative to long if/elif chains. It is ideal for matching strings against multiple patterns and is more readable than nested ifs.
OS=$(uname -s)
case "$OS" in
Linux)
echo "Linux detected"
;;
Darwin)
echo "macOS detected"
;;
CYGWIN*|MINGW*)
echo "Windows detected"
;;
*)
echo "Unknown OS: $OS"
;;
esacEach pattern can use glob wildcards (*, ?, [...]). The | operator separates multiple patterns for the same case. The ;; terminates each case block.
echo "Select an action:"
echo "1) Start"
echo "2) Stop"
echo "3) Restart"
read -r CHOICE
case "$CHOICE" in
1|start) echo "Starting..." ;;
2|stop) echo "Stopping..." ;;
3|restart) echo "Restarting..." ;;
*) echo "Invalid choice" ;;
esacCase is perfect for menu systems. The pattern 1|start matches either '1' or 'start'. The wildcard * catches all other input as invalid.
for Loops
The for loop iterates over a list of items. It is the most commonly used loop in shell scripting — perfect for processing files, arguments, or any collection.
# Iterate over a list
for color in red green blue; do
echo "$color"
done
# Iterate over files
for file in *.log; do
echo "Processing $file"
done
# Iterate over arguments
for arg in "$@"; do
echo "Argument: $arg"
done
# C-style for loop
for ((i=0; i<5; i++)); do
echo "Index: $i"
doneThe for item in list form is POSIX-compatible. The for ((expr; expr; expr)) form is Bash-specific and gives you C-style loop control.
Use "$@" (quoted) to preserve arguments with spaces. Use unquoted *.log to let glob expansion happen. Never quote glob patterns if you want them to expand.
while and until Loops
The while loop runs as long as its condition is true (exit code 0). The until loop runs as long as its condition is false (exit code non-zero). They are mirror images.
# Read a file line by line
while IFS= read -r line; do
echo "$line"
done < input.txt
# Process command output
while IFS= read -r pid; do
echo "Killing process $pid"
kill "$pid"
done < <(pgrep -f "old-process")
# Infinite loop with break
while true; do
read -r -p "Continue? (y/n) " answer
[[ "$answer" == "n" ]] && break
echo "Continuing..."
doneThe while read pattern is essential for processing line-by-line output. IFS= preserves leading whitespace. -r prevents backslash interpretation.
# Wait for a file to appear
until [[ -f /tmp/ready.flag ]]; do
echo "Waiting for ready flag..."
sleep 1
done
echo "Ready!"Until loops are less common but read more naturally for 'wait until X happens' patterns. Use whichever makes your intent clearer.
break and continue
Break exits the current loop. Continue skips to the next iteration. Both work in for, while, and until loops.
# break — exit on error condition
for file in *.csv; do
if ! head -1 "$file" | grep -q "name"; then
echo "ERROR: $file is not a CSV with headers"
break
fi
echo "Valid: $file"
done
# continue — skip problematic items
for file in *.log; do
[[ ! -r "$file" ]] && echo "Skipping unreadable: $file" && continue
echo "Processing: $file"
done
# Nested loops with break N
for i in 1 2 3; do
for j in a b c; do
[[ "$j" == "b" ]] && break 2 # Breaks both loops
echo "$i $j"
done
doneUse break to exit immediately. Use continue to skip to the next iteration. break N breaks N levels of nested loops. continue N continues at N levels up.
Always quote globs in for loops if filenames might contain spaces: for f in *.txt can break on spaces, but reading from a glob list with proper handling avoids issues. For production code, consider using find ... -print0 | while IFS= read -r -d '' to handle any filename safely.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.