Functions and Error Handling
Functions and Error Handling#
Concepts#
Functions#
Functions let you group commands under a name and reuse them. They reduce repetition and make scripts easier to read and maintain.
Defining Functions#
# Syntax 1 (preferred)
function_name() {
commands
}
# Syntax 2
function function_name {
commands
}
Calling Functions#
greet() {
echo "Hello, World!"
}
greet # call the function
greet # call it again
Function Arguments#
Functions receive arguments the same way scripts do — $1, $2, $@, $#:
greet() {
echo "Hello, $1!"
}
greet "Alice" # Hello, Alice!
greet "Bob" # Hello, Bob!
add() {
echo $(( $1 + $2 ))
}
result=$(add 5 3)
echo "5 + 3 = $result" # 5 + 3 = 8
Return Values#
Functions communicate results in two ways:
1. Exit status — return N (0-255, like exit codes):
is_root() {
if [[ $(id -u) -eq 0 ]]; then
return 0 # success/true
else
return 1 # failure/false
fi
}
if is_root; then
echo "Running as root"
else
echo "Not root"
fi
2. Output — echo the result and capture with $():
get_hostname() {
hostname
}
name=$(get_hostname)
echo "This machine is: $name"
return is for pass/fail status. echo/printf is for returning data.
Local Variables#
By default, variables in functions are global — they modify the same variables as the rest of the script. Use local to create function-scoped variables:
demo() {
local x=10 # only exists inside this function
global_y=20 # modifies the global variable
echo "Inside: x=$x"
}
x=5
demo
echo "Outside: x=$x" # Still 5 (local didn't change it)
echo "Outside: y=$global_y" # 20 (global was set)
Best practice: Always use local for variables that should not escape the function.
Error Handling#
The Problem#
By default, if a command fails, the script continues to the next command. This can cause cascading problems:
#!/bin/bash
cd /nonexistent/directory # fails silently
rm -rf * # runs in the WRONG directory!
set -e — Exit on Error#
#!/bin/bash
set -e
cd /nonexistent/directory # fails → script exits immediately
rm -rf * # never runs
set -e makes the script exit immediately when any command returns a non-zero exit code.
Caveats: Commands in if conditions, pipes, and || chains do not trigger set -e:
set -e
if ! command_that_fails; then # OK — failure is expected
echo "It failed"
fi
command_that_fails || true # OK — failure is caught
set -u — Error on Undefined Variables#
#!/bin/bash
set -u
echo "$undefined_variable" # ERROR! Script exits.
# Without set -u, this would silently expand to empty string.
set -o pipefail — Detect Pipe Failures#
By default, a pipeline’s exit status is the exit status of the last command:
false | true
echo $? # 0 (true succeeded, even though false failed!)
With pipefail, the pipeline fails if any command fails:
set -o pipefail
false | true
echo $? # 1 (false failed)
The Standard Safety Header#
Most production scripts start with:
#!/bin/bash
set -euo pipefail
This combination:
-e— exit on any error-u— error on undefined variables-o pipefail— catch failures in pipes
trap — Run Cleanup on Exit#
trap executes a command when the script receives a signal or exits:
#!/bin/bash
set -euo pipefail
TEMPFILE=$(mktemp)
# Clean up temp file when script exits (for any reason)
trap "rm -f $TEMPFILE" EXIT
echo "Working with $TEMPFILE"
echo "data" > "$TEMPFILE"
# ... do work ...
# Whether the script succeeds or fails, the trap runs and cleans up
Common trap patterns:
# Clean up on exit
trap 'rm -f "$tmpfile"' EXIT
# Clean up on error (INT = Ctrl+C, TERM = kill)
trap 'echo "Interrupted!"; cleanup; exit 1' INT TERM
# Print the failing line number on error
trap 'echo "Error on line $LINENO"' ERR
Handling Errors Manually#
# Pattern: command || handle_error
mkdir /some/dir || { echo "Failed to create directory"; exit 1; }
# Pattern: check and exit
if ! cp source.txt dest.txt; then
echo "Copy failed" >&2
exit 1
fi
# Pattern: error function
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
[[ -f "$config" ]] || die "Config file not found: $config"
Note: >&2 redirects output to stderr — error messages should go to stderr, not stdout, so they are visible even when stdout is redirected.
Putting It Together#
#!/bin/bash
set -euo pipefail
# --- Error handling ---
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
cleanup() {
rm -f "$TMPFILE"
}
trap cleanup EXIT
# --- Functions ---
check_root() {
[[ $(id -u) -eq 0 ]] || die "This script must be run as root"
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# --- Main ---
TMPFILE=$(mktemp)
check_root
log "Script started"
log "Processing data..."
# ... do work ...
log "Script completed successfully"
Lab#
Exercise 1: Basic Functions#
cd ~/lab/scripts
cat > functions.sh << 'SCRIPT'
#!/bin/bash
# A greeting function
greet() {
local name="$1"
local time_of_day
hour=$(date +%H)
if (( hour < 12 )); then
time_of_day="morning"
elif (( hour < 18 )); then
time_of_day="afternoon"
else
time_of_day="evening"
fi
echo "Good $time_of_day, $name!"
}
# A function that returns data
disk_usage() {
df / | tail -1 | awk '{print $5}' | tr -d '%'
}
# A function that returns status
is_installed() {
dpkg -l "$1" &>/dev/null
}
# --- Use the functions ---
greet "${1:-User}"
usage=$(disk_usage)
echo "Disk usage: ${usage}%"
for pkg in bash python3 nonexistent-package; do
if is_installed "$pkg"; then
echo "$pkg: installed"
else
echo "$pkg: not installed"
fi
done
SCRIPT
chmod +x functions.sh
./functions.sh
./functions.sh Alice
Exercise 2: Local vs Global Variables#
cd ~/lab/scripts
cat > scope.sh << 'SCRIPT'
#!/bin/bash
x="global"
change_x() {
local x="local"
echo "Inside function: x = $x"
}
leak_y() {
y="leaked" # no local — this is global!
echo "Inside function: y = $y"
}
echo "Before: x = $x"
change_x
echo "After change_x: x = $x" # still "global"
echo ""
echo "Before: y = ${y:-unset}"
leak_y
echo "After leak_y: y = $y" # "leaked" — it escaped!
SCRIPT
chmod +x scope.sh
./scope.sh
Exercise 3: Error Handling with set#
cd ~/lab/scripts
cat > safescript.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
echo "This script uses the safety header."
echo "It will exit on any error."
# This would fail and stop the script:
# ls /nonexistent
# Undefined variable would fail:
# echo "$undefined"
# Handle expected errors:
if ls /nonexistent 2>/dev/null; then
echo "Found it"
else
echo "Not found (but we handled it)"
fi
echo "Script completed successfully."
SCRIPT
chmod +x safescript.sh
./safescript.sh
Exercise 4: trap for Cleanup#
cd ~/lab/scripts
cat > traptest.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
TMPFILE=$(mktemp)
trap 'echo "Cleaning up $TMPFILE"; rm -f "$TMPFILE"' EXIT
echo "Created temp file: $TMPFILE"
echo "some data" > "$TMPFILE"
echo "Temp file contains: $(cat "$TMPFILE")"
echo "Script ending normally..."
# The trap runs here — cleanup happens automatically
SCRIPT
chmod +x traptest.sh
./traptest.sh
# Notice "Cleaning up..." appears at the end
Exercise 5: A Robust Script#
cd ~/lab/scripts
cat > robust.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
die() { echo "ERROR: $1" >&2; exit "${2:-1}"; }
log() { echo "[$(date +%H:%M:%S)] $1"; }
# Check arguments
[[ $# -ge 1 ]] || die "Usage: $0 <directory>"
DIR="$1"
# Validate input
[[ -d "$DIR" ]] || die "Not a directory: $DIR"
[[ -r "$DIR" ]] || die "Cannot read: $DIR"
# Do work
log "Analyzing directory: $DIR"
file_count=$(find "$DIR" -maxdepth 1 -type f | wc -l)
dir_count=$(find "$DIR" -maxdepth 1 -type d | wc -l)
((dir_count--)) # subtract the directory itself
total_size=$(du -sh "$DIR" 2>/dev/null | cut -f1)
log "Results:"
echo " Files: $file_count"
echo " Directories: $dir_count"
echo " Total size: $total_size"
log "Done."
SCRIPT
chmod +x robust.sh
./robust.sh /etc
./robust.sh /nonexistent 2>/dev/null; echo "Exit code: $?"
Review#
1. How do you define and call a function in Bash?
Define: function_name() { commands; }. Call: function_name arg1 arg2. Arguments are accessed as $1, $2, etc., just like script arguments.
2. What is the difference between `return` and `echo` in a function?
return N sets the function’s exit status (0-255) — used for pass/fail. echo prints output that can be captured with $() — used for returning data. Use return for status, echo for values.
3. Why should you use `local` for variables inside functions?
Without local, variables are global — they modify and can conflict with variables in the rest of the script. local creates a variable scoped to the function, preventing unintended side effects.
4. What does `set -euo pipefail` do?
-e: exit on any command failure. -u: treat undefined variables as errors. -o pipefail: a pipeline fails if any command in it fails (not just the last one). This combination catches most common scripting errors.
5. What is `trap` used for?
trap registers a command to run when the script exits or receives a signal. It is commonly used for cleanup (deleting temp files, releasing locks) to ensure cleanup happens even if the script fails.
6. Why should error messages be sent to stderr (`>&2`)?
So they are visible even when stdout is redirected to a file or pipe. If errors go to stdout, they get mixed into the data stream and may go unnoticed.
Previous: Loops | Next: Practical Scripts