🔄 Lesson 8.3: Loops and Functions
Repeat actions efficiently and organize your code into reusable building blocks.
🎯 Learning Objectives
- Iterate over lists and ranges with
forloops - Repeat actions with
whileanduntilloops - Control loop flow with
breakandcontinue - Define and call functions
- Pass arguments to functions and return values
- Use local variables to avoid side effects
- Build a complete, practical script combining everything
Estimated Time: 45 minutes
📑 In This Lesson
for Loops
A for loop repeats commands for each item in a list.
Looping Over a List
#!/bin/bash
# Loop over explicit values
for COLOR in red green blue; do
echo "Color: $COLOR"
done
# Loop over files in a directory
for FILE in *.txt; do
echo "Found text file: $FILE"
done
# Loop over command output
for USER in $(who | awk '{print $1}' | sort -u); do
echo "Logged in: $USER"
done
C-Style for Loop
# Count from 1 to 10
for ((i = 1; i <= 10; i++)); do
echo "Number: $i"
done
# Count by 2s
for ((i = 0; i <= 20; i += 2)); do
echo "$i"
done
Using {start..end} Ranges
# Brace expansion — generates a sequence
for i in {1..5}; do
echo "Step $i"
done
# With a step value (Bash 4+)
for i in {0..100..10}; do
echo "$i%"
done
# Letters work too
for letter in {a..z}; do
echo -n "$letter "
done
echo ""
with current item"] C --> D{"More items?"} D -->|Yes| B D -->|No| E["Done"] style A fill:#3b82f6,stroke:#2563eb,color:#fff style B fill:#6366f1,stroke:#4338ca,color:#fff style C fill:#f59e0b,stroke:#d97706,color:#fff style D fill:#6366f1,stroke:#4338ca,color:#fff style E fill:#22c55e,stroke:#166534,color:#fff
⚠️ Don't Loop Over ls Output
Filenames with spaces break for f in $(ls). Use glob patterns instead:
# ❌ Fragile — breaks on filenames with spaces
for f in $(ls *.txt); do echo "$f"; done
# ✅ Correct — glob handles spaces properly
for f in *.txt; do echo "$f"; done
while Loops
A while loop repeats as long as a condition is true.
#!/bin/bash
# Count down from 5
COUNT=5
while [ "$COUNT" -gt 0 ]; do
echo "$COUNT..."
((COUNT--))
done
echo "Liftoff!"
# Read a file line by line (the proper way)
while IFS= read -r LINE; do
echo "Line: $LINE"
done < /etc/hostname
# Infinite loop with exit condition
while true; do
read -p "Enter 'quit' to exit: " INPUT
if [ "$INPUT" = "quit" ]; then
break
fi
echo "You said: $INPUT"
done
💡 Reading Files Line by Line
The while IFS= read -r LINE pattern is the safe way to process files line by line:
IFS=— prevents leading/trailing whitespace from being stripped-r— prevents backslash interpretation< file— redirects the file into the loop's stdin
Processing Command Output with a Pipe
# Process each line from a command
df -h | tail -n +2 | while read -r LINE; do
USAGE=$(echo "$LINE" | awk '{print $5}' | tr -d '%')
MOUNT=$(echo "$LINE" | awk '{print $6}')
if [ "$USAGE" -gt 80 ] 2>/dev/null; then
echo "⚠️ WARNING: $MOUNT is ${USAGE}% full!"
fi
done
until Loops
until is the opposite of while — it loops until the condition becomes true.
#!/bin/bash
# Wait for a file to appear
echo "Waiting for /tmp/signal.txt to be created..."
until [ -f /tmp/signal.txt ]; do
sleep 1
done
echo "File found!"
# Count up to 5
COUNT=1
until [ "$COUNT" -gt 5 ]; do
echo "Count: $COUNT"
((COUNT++))
done
💡 while vs until
They're interchangeable — until [ condition ] is the same as while [ ! condition ]. Use whichever reads more naturally. In practice, while is used far more often. until shines for "wait for something to happen" patterns.
break and continue
#!/bin/bash
# break — exit the loop entirely
for i in {1..100}; do
if [ "$i" -gt 5 ]; then
break # Stop at 5
fi
echo "$i"
done
# Output: 1 2 3 4 5
# continue — skip the rest of THIS iteration
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue # Skip even numbers
fi
echo "$i"
done
# Output: 1 3 5 7 9
Functions
Functions let you group commands into reusable blocks with a name. Define once, call anywhere.
#!/bin/bash
# Define a function (two equivalent syntaxes)
greet() {
echo "Hello, World!"
}
function say_goodbye {
echo "Goodbye!"
}
# Call the functions
greet # Hello, World!
say_goodbye # Goodbye!
# Functions must be defined BEFORE they're called
# (Bash reads top-to-bottom)
💡 Function Naming
Use lowercase_with_underscores for function names — the standard Bash convention. The function keyword is optional; name() { } is the POSIX-compatible form and works everywhere.
Function Arguments and Return Values
Functions receive arguments the same way scripts do — via $1, $2, $@, etc.
#!/bin/bash
greet() {
local name="${1:-World}" # First argument, default "World"
echo "Hello, $name!"
}
greet "Ray" # Hello, Ray!
greet # Hello, World!
# A function with multiple arguments
add() {
echo $(( $1 + $2 ))
}
RESULT=$(add 10 20)
echo "Sum: $RESULT" # Sum: 30
Return Values
Bash functions have two ways to "return" data:
#!/bin/bash
# Method 1: return — sets an exit code (0-255 only)
is_even() {
if [ $(( $1 % 2 )) -eq 0 ]; then
return 0 # Success = true
else
return 1 # Failure = false
fi
}
if is_even 42; then
echo "42 is even" # This runs
fi
# Method 2: echo — outputs text (capture with $())
get_extension() {
local filename="$1"
echo "${filename##*.}" # Parameter expansion: remove everything before last dot
}
EXT=$(get_extension "report.pdf")
echo "Extension: $EXT" # Extension: pdf
🐧 return vs echo
return sets the exit status (like exit but for functions, not the whole script). Use it for success/failure signals. echo (captured with $()) is how you return actual data — strings, numbers, computed results. Most real-world functions use echo.
Local Variables
By default, variables in Bash are global — even inside functions. Use local to keep variables contained.
#!/bin/bash
GLOBAL_VAR="I'm global"
my_function() {
local LOCAL_VAR="I'm local"
GLOBAL_VAR="Modified by function" # Changes the global!
echo "Inside: LOCAL_VAR=$LOCAL_VAR"
echo "Inside: GLOBAL_VAR=$GLOBAL_VAR"
}
my_function
echo "Outside: LOCAL_VAR=$LOCAL_VAR" # Empty — local is gone
echo "Outside: GLOBAL_VAR=$GLOBAL_VAR" # "Modified by function"
Output
Inside: LOCAL_VAR=I'm local
Inside: GLOBAL_VAR=Modified by function
Outside: LOCAL_VAR=
Outside: GLOBAL_VAR=Modified by function
⚠️ Always Use local in Functions
Without local, a variable inside a function can accidentally overwrite a variable with the same name in the calling code. Always declare function variables with local unless you specifically intend to modify a global.
Capstone Script: Project Initializer
Here's a complete script that uses everything from this module — variables, input, conditionals, loops, and functions:
#!/bin/bash
# ============================================
# init-project.sh — Set up a new project directory
# Usage: ./init-project.sh [project-name]
# ============================================
# --- Configuration ---
PROJECTS_DIR="$HOME/projects"
TEMPLATES=("README.md" ".gitignore" "LICENSE")
# --- Functions ---
print_header() {
echo ""
echo "=============================="
echo " 🚀 Project Initializer"
echo "=============================="
echo ""
}
create_readme() {
local name="$1"
local dir="$2"
cat > "$dir/README.md" << EOF
# $name
## Description
A new project.
## Getting Started
\`\`\`bash
cd $name
\`\`\`
## Author
$(whoami)
## Created
$(date +%Y-%m-%d)
EOF
}
create_gitignore() {
local dir="$1"
cat > "$dir/.gitignore" << 'EOF'
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
.vscode/
.idea/
# Dependencies
node_modules/
__pycache__/
*.pyc
# Build output
dist/
build/
EOF
}
create_subdirs() {
local dir="$1"
shift # Remove first argument, leaving the rest
for subdir in "$@"; do
mkdir -p "$dir/$subdir"
echo " 📁 Created $subdir/"
done
}
# --- Main Script ---
print_header
# Get project name from argument or prompt
PROJECT_NAME="${1:-}"
if [ -z "$PROJECT_NAME" ]; then
read -p "Project name: " PROJECT_NAME
fi
# Validate name (letters, numbers, hyphens, underscores only)
if [[ ! "$PROJECT_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "❌ Invalid name. Use only letters, numbers, hyphens, underscores." >&2
exit 1
fi
PROJECT_DIR="$PROJECTS_DIR/$PROJECT_NAME"
# Check if it already exists
if [ -d "$PROJECT_DIR" ]; then
echo "❌ Directory already exists: $PROJECT_DIR" >&2
exit 1
fi
# Ask what type of project
echo "What kind of project?"
echo " 1) Basic (just README and .gitignore)"
echo " 2) Web (adds src/, css/, js/ directories)"
echo " 3) Python (adds src/, tests/, requirements.txt)"
read -p "Choice [1]: " PROJECT_TYPE
PROJECT_TYPE="${PROJECT_TYPE:-1}"
# Create the project
echo ""
echo "Creating project: $PROJECT_NAME"
echo "Location: $PROJECT_DIR"
echo "---"
mkdir -p "$PROJECT_DIR"
echo " 📁 Created project directory"
# Create base files
create_readme "$PROJECT_NAME" "$PROJECT_DIR"
echo " 📄 Created README.md"
create_gitignore "$PROJECT_DIR"
echo " 📄 Created .gitignore"
# Create type-specific structure
case "$PROJECT_TYPE" in
2)
create_subdirs "$PROJECT_DIR" "src" "css" "js" "images"
touch "$PROJECT_DIR/src/index.html"
echo " 📄 Created src/index.html"
;;
3)
create_subdirs "$PROJECT_DIR" "src" "tests"
touch "$PROJECT_DIR/requirements.txt"
touch "$PROJECT_DIR/src/__init__.py"
echo " 📄 Created requirements.txt"
echo " 📄 Created src/__init__.py"
;;
*)
# Basic — no extra directories
;;
esac
# Initialize git if available
if command -v git &> /dev/null; then
cd "$PROJECT_DIR" && git init --quiet
echo " 🔀 Initialized git repository"
fi
# Done!
echo ""
echo "✅ Project '$PROJECT_NAME' is ready!"
echo " cd $PROJECT_DIR"
💡 Concepts Used
- Functions:
print_header,create_readme,create_gitignore,create_subdirs - Variables & defaults:
${1:-},${PROJECT_TYPE:-1} - Conditionals:
if,[[ regex ]],case - Loops:
for subdir in "$@" - Local variables:
local name="$1" - Heredocs:
cat > file << EOF - Exit codes:
exit 1on errors
Exercises
🏋️ Exercise 1: Loop Practice
- Write a
forloop that prints the multiplication table for 7 (7×1 through 7×12) - Write a
whileloop that reads/etc/passwdline by line and counts the lines - Write a
forloop that renames all.txtfiles in a directory to.bak
🏋️ Exercise 2: Function Library
Create a file called utils.sh with these functions:
to_upper "text"— converts text to uppercaseto_lower "text"— converts text to lowercasefile_size "path"— returns the size of a file in human-readable formatconfirm "question"— asks yes/no, returns 0 for yes, 1 for no
Hint: source utils.sh to load it into another script!
🏋️ Exercise 3: Batch File Processor
Write a script that:
- Takes a directory as an argument
- Loops through all
.logfiles in that directory - For each file, counts the lines and prints the filename + count
- At the end, prints the total line count across all files
- Uses a function for counting lines in a single file
🏋️ Exercise 4: The Complete Guessing Game
Build on the guessing game from Lesson 19. Add:
- A function
play_round()that handles one game - A function
show_stats()that tracks best score across rounds - A
whileloop that asks "Play again?" after each round - Input validation (reject non-numbers)
- A difficulty selector: Easy (1-50), Medium (1-100), Hard (1-500)
Knowledge Check
❓ Question 1
What's the safe way to loop over files in a directory?
❓ Question 2
What does break do inside a loop?
❓ Question 3
Why should you use local for variables inside functions?
❓ Question 4
How do you return a string value from a function?
Summary
🎉 Key Takeaways
for item in listiterates over items;for ((i=0; i<n; i++))for C-style countingwhile [ condition ]repeats while true;until [ condition ]repeats until truewhile IFS= read -r lineis the safe way to process files line by linebreakexits a loop;continueskips to the next iteration- Functions:
name() { commands; }— define before calling - Function arguments use
$1,$2,$@— same as script arguments returnsets exit status (0-255);echo+$()returns actual data- Always use
localfor variables inside functions
🍎 On macOS
Loops and functions in this lesson all work with #!/bin/bash on macOS. Note that the seq command works on macOS, but {1..10} brace expansion for ranges also works and avoids spawning an extra process. If you installed Bash 5 via Homebrew (brew install bash), you'll also have access to declare -n (namerefs) and other Bash 4+/5 features.
🎓 Course Complete!
Congratulations! You've completed Linux for Beginners. You now have the skills to navigate the terminal, manage files and permissions, install software, administer services, connect to remote machines, and automate tasks with shell scripts. Whether you're on Ubuntu, WSL, or macOS, these skills transfer everywhere. Keep practicing, keep exploring, and remember — the terminal is your superpower.
🚀 Where to Go from Here
- Practice daily — use the terminal for everyday tasks, even when a GUI exists
- Read man pages —
man commandis your always-available reference - Explore further — cron jobs, advanced shell scripting, Docker, and Linux server administration
- Contribute — many open-source projects welcome Linux-savvy contributors