Lesson 5: Functions & Modularity
Goal: Write reusable, organized code with functions, return values, and script sourcing.
Table of Contents
- Defining Functions
- Arguments in Functions
- Return Values
- Local Variables
- Sourcing Other Scripts
- Common Function Patterns
- Exercises
Defining Functions
Two syntaxes (both work)
# Style 1 — recommended
greet() {
echo "Hello, World!"
}
# Style 2 — also valid
function greet {
echo "Hello, World!"
}
# Call the function
greet
Functions must be defined before use
#!/bin/bash
# WRONG — function not yet defined
greet # Error: command not found
greet() {
echo "Hello"
}
#!/bin/bash
# CORRECT — define first, call later
greet() {
echo "Hello"
}
greet # Works!
Arguments in Functions
Functions receive arguments the same way scripts do: $1, $2, $@, $#.
greet() {
local name="$1"
local greeting="${2:-Hello}"
echo "$greeting, $name!"
}
greet "Benjamin" # Hello, Benjamin!
greet "Benjamin" "Hi" # Hi, Benjamin!
Forward all arguments
wrapper() {
echo "Running with args: $@"
some_command "$@"
}
wrapper --flag value --other
Check argument count
create_user() {
if [[ $# -lt 2 ]]; then
echo "Usage: create_user <username> <email>" >&2
return 1
fi
local username="$1"
local email="$2"
echo "Creating user: $username ($email)"
}
create_user "alice" "alice@example.com"
create_user "bob" # Error: Usage message
Return Values
Exit codes (0 = success, 1-255 = error)
is_even() {
local num="$1"
if (( num % 2 == 0 )); then
return 0 # success = true
else
return 1 # failure = false
fi
}
if is_even 42; then
echo "42 is even"
fi
if ! is_even 7; then
echo "7 is odd"
fi
Capture output (for returning data)
get_hostname() {
hostname -f
}
# Capture output into a variable
MY_HOST=$(get_hostname)
echo "Host: $MY_HOST"
Return complex data
get_system_info() {
echo "$(hostname)|$(uname -r)|$(uptime -p)"
}
INFO=$(get_system_info)
IFS='|' read -r HOST KERNEL UPTIME <<< "$INFO"
echo "Host: $HOST"
echo "Kernel: $KERNEL"
echo "Uptime: $UPTIME"
Local Variables
Without local — variables leak
set_name() {
NAME="Alice" # GLOBAL — modifies outer scope!
}
NAME="Benjamin"
set_name
echo "$NAME" # Alice — outer variable was changed!
With local — variables are scoped
set_name() {
local NAME="Alice" # LOCAL — only inside this function
echo "Inside: $NAME"
}
NAME="Benjamin"
set_name # Inside: Alice
echo "Outside: $NAME" # Outside: Benjamin — unchanged!
Rule: Always use local for variables inside functions.
process_file() {
local file="$1"
local line_count
local size
line_count=$(wc -l < "$file")
size=$(du -h "$file" | cut -f1)
echo "$file: $line_count lines, $size"
}
Sourcing Other Scripts
Split large scripts into reusable modules.
Create a library file
# lib/utils.sh
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
error() {
echo "[ERROR] $*" >&2
}
die() {
error "$@"
exit 1
}
require_command() {
command -v "$1" &>/dev/null || die "$1 is required but not installed"
}
Source it in your script
#!/bin/bash
set -euo pipefail
# Source the library (relative to script location)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/utils.sh"
# Now use the functions
log "Starting deployment"
require_command docker
require_command git
log "All checks passed"
Guard against double-sourcing
# lib/utils.sh
[[ -n "${_UTILS_LOADED:-}" ]] && return
_UTILS_LOADED=1
log() { echo "[$(date '+%H:%M:%S')] $*"; }
# ... rest of functions
Common Function Patterns
Logging functions
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
log_info "Starting process"
log_warn "Disk space low"
log_error "Connection failed"
Cleanup with trap
cleanup() {
local exit_code=$?
echo "Cleaning up temporary files..."
rm -rf "$TMP_DIR"
exit "$exit_code"
}
TMP_DIR=$(mktemp -d)
trap cleanup EXIT
# Work with TMP_DIR... cleanup runs automatically on exit
echo "Working in $TMP_DIR"
Confirmation prompt
confirm() {
local message="${1:-Are you sure?}"
read -rp "$message [y/N] " response
[[ "$response" =~ ^[Yy]$ ]]
}
if confirm "Delete all logs?"; then
rm -f /var/log/myapp/*.log
echo "Logs deleted"
else
echo "Cancelled"
fi
Retry function
retry() {
local max_attempts="${1:-3}"
local delay="${2:-2}"
shift 2
local cmd=("$@")
for ((attempt=1; attempt<=max_attempts; attempt++)); do
if "${cmd[@]}"; then
return 0
fi
echo "Attempt $attempt/$max_attempts failed. Retrying in ${delay}s..."
sleep "$delay"
done
echo "All $max_attempts attempts failed" >&2
return 1
}
# Usage
retry 5 3 curl -sf https://api.example.com/health
Exercises
Exercise 1: Calculator
cat > calc.sh << 'EOF'
#!/bin/bash
set -euo pipefail
add() { echo $(($1 + $2)); }
subtract() { echo $(($1 - $2)); }
multiply() { echo $(($1 * $2)); }
divide() {
if [[ "$2" -eq 0 ]]; then
echo "Error: division by zero" >&2
return 1
fi
echo $(($1 / $2))
}
if [[ $# -ne 3 ]]; then
echo "Usage: $0 <num1> <operator> <num2>"
echo "Operators: add, sub, mul, div"
exit 1
fi
case "$2" in
add) result=$(add "$1" "$3") ;;
sub) result=$(subtract "$1" "$3") ;;
mul) result=$(multiply "$1" "$3") ;;
div) result=$(divide "$1" "$3") ;;
*) echo "Unknown operator: $2" >&2; exit 1 ;;
esac
echo "$1 $2 $3 = $result"
EOF
chmod +x calc.sh
./calc.sh 10 add 5
./calc.sh 20 mul 3
./calc.sh 100 div 0
rm calc.sh
Exercise 2: Library pattern
# Create library
mkdir -p lib
cat > lib/colors.sh << 'EOF'
[[ -n "${_COLORS_LOADED:-}" ]] && return
_COLORS_LOADED=1
info() { echo -e "\033[32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[33m[WARN]\033[0m $*"; }
error() { echo -e "\033[31m[ERROR]\033[0m $*" >&2; }
success() { echo -e "\033[32m[OK]\033[0m $*"; }
EOF
# Create main script
cat > deploy.sh << 'EOF'
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/lib/colors.sh"
info "Starting deployment..."
warn "This is a dry run"
success "All checks passed"
error "Just kidding, this is a demo error"
EOF
chmod +x deploy.sh && ./deploy.sh
rm -rf deploy.sh lib/
Key Takeaways
- Define functions before calling them
- Use
localfor all variables inside functions returnsets exit code (0/1); use$(func)to capture outputsource file.shloads functions into current shell- Common patterns: logging, cleanup with
trap, retry, confirmation - Split large scripts into library files for reusability
- Always pass arguments with
"$@"to preserve quoting
Next Lesson: Lesson 6: Arrays & Data Structures →