Defining Functions#
# Standard syntax
greet() {
local name="${1:?Error: name required}"
local greeting="${2:-Hello}"
echo "${greeting}, ${name}!"
}
# Alternative syntax (less common)
function greet {
echo "Hello, $1"
}The function keyword is optional in bash. Prefer the name() syntax — it’s POSIX-compatible.
Parameters and Return Values#
Positional Parameters#
create_user() {
local username="$1"
local email="$2"
local role="${3:-viewer}" # default value
echo "Creating $username ($email) with role: $role"
}
create_user "alice" "alice@example.com" "admin"
create_user "bob" "bob@example.com" # role defaults to "viewer"Parameter Validation#
deploy() {
local env="${1:?Error: environment required (test|pre|prod)}"
local version="${2:?Error: version required}"
[[ "$env" =~ ^(test|pre|prod)$ ]] || {
echo "Invalid environment: $env" >&2
return 1
}
echo "Deploying v$version to $env"
}Return Values#
Functions return exit codes (0-255), not data. Use echo + command substitution for data:
# Exit codes for success/failure
validate_email() {
local email="$1"
[[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}
if validate_email "$user_email"; then
echo "Valid"
else
echo "Invalid" >&2
fi
# Echo for returning data
get_timestamp() {
date +%s
}
ts=$(get_timestamp)
# Multiple values — use a delimiter or global variables
get_dimensions() {
echo "1920 1080"
}
read -r width height <<< "$(get_dimensions)"Local Variables#
Without local, variables leak to the calling scope:
# BAD — leaks to caller
bad_function() {
x=42 # modifies global scope!
}
x=10
bad_function
echo "$x" # 42 (unexpected!)
# GOOD — scoped to function
good_function() {
local x=42
}
x=10
good_function
echo "$x" # 10 (preserved)Rule: Always use local for every variable inside a function.
Error Handling Patterns#
Die Function#
die() {
echo "ERROR: $*" >&2
exit 1
}
# Usage
[[ -f "$config" ]] || die "Config not found: $config"
cd "$work_dir" || die "Cannot access: $work_dir"Logging Functions#
readonly RED='\033[0;31m'
readonly YELLOW='\033[0;33m'
readonly GREEN='\033[0;32m'
readonly NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
log_info "Starting deployment"
log_warn "Disk space below 20%"
log_error "Connection failed"Retry Pattern#
retry() {
local max_attempts="${1:?}"
local delay="${2:?}"
shift 2
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if "$@"; then
return 0
fi
log_warn "Attempt $attempt/$max_attempts failed. Retrying in ${delay}s..."
sleep "$delay"
((attempt++))
done
log_error "All $max_attempts attempts failed"
return 1
}
# Usage
retry 3 5 curl -sf "http://api.example.com/health"Function Libraries#
Organize reusable functions in separate files:
# lib/logging.sh
log_info() { echo "[INFO] $(date '+%H:%M:%S') $*"; }
log_error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }
# lib/validation.sh
require_command() {
command -v "$1" &>/dev/null || die "$1 is required but not installed"
}
require_var() {
[[ -n "${!1:-}" ]] || die "Variable $1 must be set"
}
# main.sh
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/lib/logging.sh"
source "$(dirname "$0")/lib/validation.sh"
require_command docker
require_var API_KEY
log_info "All checks passed"Advanced Patterns#
Function as Argument (Callbacks)#
apply_to_files() {
local callback="$1"
shift
for file in "$@"; do
"$callback" "$file"
done
}
compress() { gzip "$1"; }
apply_to_files compress *.logCleanup with Trap#
main() {
local tmpdir
tmpdir=$(mktemp -d)
trap "rm -rf '$tmpdir'" EXIT
# Work in tmpdir safely — cleanup guaranteed
cp data.txt "$tmpdir/"
process "$tmpdir/data.txt"
}
main "$@"Key Takeaways#
- Always use
localfor function variables - Return exit codes for success/failure (0 = success)
- Use
echo+ command substitution to return data - Validate parameters early with
${1:?message} - Create a
die()function in every script - Source library files to share functions across scripts
- Use
"$@"to pass all arguments through to another command