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"
}