Bash Functions
Function definition, scope, and modular scripting patterns.
Function Definition Styles
# POSIX style (portable)
my_function() {
echo "POSIX style"
}
# Bash style (explicit)
function my_function {
echo "Bash keyword style"
}
# POSIX with explicit local scope
my_function() {
local var="local to function"
echo "$var"
}
Argument Handling
# Positional parameters
process_host() {
local hostname="$1"
local ip="$2"
local port="${3:-22}" # Default value
echo "Host: $hostname, IP: $ip, Port: $port"
}
process_host "vault-01" "10.50.1.60" # Port defaults to 22
process_host "vault-01" "10.50.1.60" 8200 # Override port
# All arguments
log_all() {
echo "Received $# arguments:"
for arg in "$@"; do # "$@" preserves quoting
echo " - $arg"
done
}
log_all "arg with spaces" "another arg"
# Shift through arguments
process_flags() {
while (( $# > 0 )); do
case "$1" in
-v|--verbose) VERBOSE=true ;;
-h|--help) show_help; return 0 ;;
--) shift; break ;;
-*) echo "Unknown option: $1" >&2; return 1 ;;
*) break ;;
esac
shift
done
# Remaining args in "$@"
echo "Remaining: $*"
}
# Named parameters via associative array
deploy_service() {
local -A opts
while (( $# > 0 )); do
case "$1" in
--name) opts[name]="$2"; shift 2 ;;
--image) opts[image]="$2"; shift 2 ;;
--replicas) opts[replicas]="$2"; shift 2 ;;
*) echo "Unknown: $1" >&2; return 1 ;;
esac
done
echo "Deploying ${opts[name]} with ${opts[image]} (${opts[replicas]:-1} replicas)"
}
deploy_service --name web --image nginx:latest --replicas 3
Return Values and Exit Codes
# Exit codes (0 = success, 1-255 = failure)
check_host() {
local host="$1"
ping -c1 -W2 "$host" &>/dev/null
return $? # Propagate ping's exit code
}
if check_host "vault-01"; then
echo "Host is up"
else
echo "Host is down (exit code: $?)"
fi
# Return string via stdout (preferred)
get_ip() {
local hostname="$1"
host "$hostname" | awk '/has address/ {print $NF}'
}
ip=$(get_ip "vault-01.inside.domusdigitalis.dev")
# Return array via nameref (bash 4.3+)
get_hosts() {
local -n result=$1 # Nameref to caller's variable
result=("vault-01" "ise-01" "bind-01")
}
declare -a my_hosts
get_hosts my_hosts
echo "${my_hosts[@]}"
# Return multiple values via global (use sparingly)
parse_connection() {
local conn="$1"
# Sets globals
CONN_HOST="${conn%:*}"
CONN_PORT="${conn##*:}"
}
parse_connection "vault-01:8200"
echo "Host: $CONN_HOST, Port: $CONN_PORT"
# Return associative array via nameref
get_system_info() {
local -n info=$1
info[hostname]=$(hostname)
info[kernel]=$(uname -r)
info[uptime]=$(uptime -p)
info[load]=$(awk '{print $1}' /proc/loadavg)
}
declare -A sysinfo
get_system_info sysinfo
echo "Host: ${sysinfo[hostname]}, Load: ${sysinfo[load]}"
Variable Scope
# Local vs Global
GLOBAL_VAR="visible everywhere"
my_function() {
local local_var="only in function"
GLOBAL_VAR="modified by function" # Modifies global!
# Create new global from inside function (avoid this)
NEW_GLOBAL="created inside function"
}
my_function
echo "$GLOBAL_VAR" # "modified by function"
echo "$local_var" # Empty - local is gone
echo "$NEW_GLOBAL" # "created inside function"
# Subshell isolation
outer_var="original"
(
outer_var="changed in subshell"
echo "Inside subshell: $outer_var" # "changed in subshell"
)
echo "After subshell: $outer_var" # "original" (unchanged!)
# Capture subshell modifications
result=$(
compute_value
echo "$computed"
)
# Dynamic scoping danger
set_debug() {
DEBUG=true # Sets caller's DEBUG!
}
DEBUG=false
set_debug
echo "$DEBUG" # true (modified!)
# Safe pattern: always use local
safe_function() {
local DEBUG="${DEBUG:-false}" # Shadow, don't modify
DEBUG=true # Only affects local
do_stuff
}
Error Handling Patterns
# Die function
die() {
echo "ERROR: $*" >&2
exit 1
}
# Assert function
assert() {
local condition="$1"
local message="${2:-Assertion failed}"
if ! eval "$condition"; then
die "$message"
fi
}
assert '[ -f /etc/passwd ]' "passwd file missing"
# Try-catch pattern
try() {
set -e
"$@"
local exit_code=$?
set +e
return $exit_code
}
catch() {
local exit_code=$?
"$@"
return $exit_code
}
try risky_operation || catch handle_error
# Cleanup on exit (trap)
cleanup() {
local exit_code=$?
rm -f "$TEMP_FILE"
echo "Cleaned up, exiting with code $exit_code"
}
trap cleanup EXIT
# Stack trace on error
enable_trace() {
set -eE
trap 'echo "Error at ${BASH_SOURCE[0]}:${LINENO} in ${FUNCNAME[0]:-main}"' ERR
}
# Validate arguments
require_args() {
local func_name="$1"
local required="$2"
local actual="$3"
if (( actual < required )); then
die "$func_name requires $required arguments, got $actual"
fi
}
deploy() {
require_args "deploy" 2 $#
local service="$1"
local version="$2"
# ...
}
Infrastructure Automation Patterns
# SSH wrapper with retry
ssh_retry() {
local host="$1"
local cmd="$2"
local max_attempts="${3:-3}"
local delay="${4:-5}"
local attempt=1
while (( attempt <= max_attempts )); do
if ssh -o ConnectTimeout=5 "$host" "$cmd" 2>/dev/null; then
return 0
fi
echo "Attempt $attempt failed, retrying in ${delay}s..." >&2
((attempt++))
sleep "$delay"
done
return 1
}
ssh_retry "vault-01" "vault status" 5 10
# Parallel execution with results
parallel_check() {
local -a hosts=("$@")
local -A results
for host in "${hosts[@]}"; do
(
if ping -c1 -W2 "$host" &>/dev/null; then
echo "$host:UP"
else
echo "$host:DOWN"
fi
) &
done | while IFS=: read -r host status; do
results["$host"]="$status"
echo "${host}: ${status}"
done
wait
}
parallel_check vault-01 ise-01 bind-01 kvm-01
# Config loader
load_env() {
local env_file="${1:-.env}"
if [[ ! -f "$env_file" ]]; then
echo "Warning: $env_file not found" >&2
return 1
fi
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
# Export, handling quoted values
value="${value%\"}"
value="${value#\"}"
export "$key=$value"
done < "$env_file"
}
# Logging functions
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
LOG_LEVEL="${LOG_LEVEL:-INFO}"
log() {
local level="$1"; shift
local level_num="${LOG_LEVELS[$level]:-1}"
local current_num="${LOG_LEVELS[$LOG_LEVEL]:-1}"
if (( level_num >= current_num )); then
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $*" >&2
fi
}
log_debug() { log DEBUG "$@"; }
log_info() { log INFO "$@"; }
log_warn() { log WARN "$@"; }
log_error() { log ERROR "$@"; }
# Usage
LOG_LEVEL=DEBUG
log_debug "Detailed debugging info"
log_info "Normal operation"
log_error "Something went wrong"
API Wrapper Functions
# REST API wrapper
api_call() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
local base_url="${API_BASE_URL:?API_BASE_URL not set}"
local token="${API_TOKEN:?API_TOKEN not set}"
local curl_args=(
-s
-X "$method"
-H "Authorization: Bearer $token"
-H "Content-Type: application/json"
)
[[ -n "$data" ]] && curl_args+=(-d "$data")
curl "${curl_args[@]}" "${base_url}${endpoint}"
}
# ISE session lookup
ise_get_session() {
local mac="$1"
netapi ise mnt sessions --format json | \
jq -r --arg mac "$mac" '.[] | select(.calling_station_id == $mac)'
}
# Vault secret getter with caching
declare -A _vault_cache
vault_get() {
local path="$1"
local field="${2:-value}"
local cache_key="${path}:${field}"
if [[ -v _vault_cache[$cache_key] ]]; then
echo "${_vault_cache[$cache_key]}"
return 0
fi
local value
value=$(vault kv get -field="$field" "$path" 2>/dev/null) || return 1
_vault_cache["$cache_key"]="$value"
echo "$value"
}
# k8s resource checker
k8s_wait_ready() {
local resource="$1"
local namespace="${2:-default}"
local timeout="${3:-300}"
log_info "Waiting for $resource in $namespace..."
kubectl wait --for=condition=ready "$resource" \
-n "$namespace" \
--timeout="${timeout}s" 2>/dev/null
}
k8s_wait_ready "pod/wazuh-manager-master-0" "wazuh" 120
Composable Function Design
# Higher-order functions
map() {
local func="$1"; shift
local -a results=()
for item in "$@"; do
results+=("$($func "$item")")
done
printf '%s\n' "${results[@]}"
}
filter() {
local predicate="$1"; shift
for item in "$@"; do
if $predicate "$item"; then
echo "$item"
fi
done
}
reduce() {
local func="$1"
local acc="$2"; shift 2
for item in "$@"; do
acc=$($func "$acc" "$item")
done
echo "$acc"
}
# Usage
to_upper() { echo "${1^^}"; }
is_running() { systemctl is-active "$1" &>/dev/null; }
sum() { echo $(($1 + $2)); }
map to_upper vault-01 ise-01 bind-01
# VAULT-01\nISE-01\nBIND-01
filter is_running sshd nginx docker
# sshd\nnginx (if running)
reduce sum 0 1 2 3 4 5
# 15
# Pipeline-friendly functions
to_json_array() {
local first=true
echo -n "["
while read -r item; do
$first || echo -n ","
echo -n "\"$item\""
first=false
done
echo "]"
}
printf '%s\n' vault-01 ise-01 bind-01 | to_json_array
# ["vault-01","ise-01","bind-01"]
# Decorator pattern
with_timing() {
local func="$1"; shift
local start end duration
start=$(date +%s.%N)
"$func" "$@"
local exit_code=$?
end=$(date +%s.%N)
duration=$(echo "$end - $start" | bc)
log_debug "$func completed in ${duration}s"
return $exit_code
}
with_retry() {
local max_attempts="$1"
local func="$2"; shift 2
local attempt=1
while (( attempt <= max_attempts )); do
if "$func" "$@"; then
return 0
fi
((attempt++))
sleep 1
done
return 1
}
# Combine decorators
with_timing with_retry 3 risky_api_call
Function Gotchas
# WRONG: Function called in subshell can't modify parent
my_func() { RESULT="modified"; }
RESULT="original"
echo "$(my_func)" # RESULT still "original"!
# CORRECT: Call function directly, capture if needed
my_func
echo "$RESULT" # "modified"
# WRONG: Forgetting to quote arguments
process() { echo "Got: $1"; }
process file with spaces.txt # $1 = "file" only!
# CORRECT: Quote at call site
process "file with spaces.txt" # $1 = "file with spaces.txt"
# WRONG: Using $@ in arithmetic
count_args() {
echo $(( $@ )) # Syntax error!
}
# CORRECT: Use $#
count_args() {
echo "$#"
}
# WRONG: Returning string via return
get_name() {
return "hostname" # Error! return only takes integers
}
# CORRECT: Echo to stdout, capture with $()
get_name() {
echo "hostname"
}
name=$(get_name)
# WRONG: Local without initial value inherits global
VAR="global"
my_func() {
local VAR # VAR is now empty, not "global"!
echo "$VAR"
}
# Pattern: Explicit initialization
my_func() {
local VAR="${VAR:-default}" # Inherit or default
}
# WRONG: Function name conflicts with command
test() { # Shadows /usr/bin/test!
echo "My test"
}
# CORRECT: Use prefixes or longer names
my_test() {
echo "My test"
}