Bash Arrays

Indexed and associative array operations.

Indexed Arrays - Fundamentals

# Declaration methods
declare -a hosts=()                          # Empty array
hosts=("vault-01" "ise-01" "bind-01")        # Inline declaration
readarray -t hosts < /tmp/hostlist.txt       # From file (newline-delimited)
hosts=( $(cat /tmp/hostlist.txt) )           # Command substitution (UNSAFE with spaces)

# Element access
echo "${hosts[0]}"                           # First element
echo "${hosts[-1]}"                          # Last element (bash 4.3+)
echo "${hosts[@]}"                           # All elements (preserves quoting)
echo "${hosts[*]}"                           # All elements (single string)

# Metadata
echo "${#hosts[@]}"                          # Array length
echo "${#hosts[0]}"                          # Length of first element
echo "${!hosts[@]}"                          # All indices (0 1 2 ...)

Array Slicing and Substring Extraction

arr=("zero" "one" "two" "three" "four" "five")

# Slicing: ${array[@]:start:count}
echo "${arr[@]:2:3}"                         # "two three four" (3 elements from index 2)
echo "${arr[@]:3}"                           # "three four five" (from index 3 to end)
echo "${arr[@]: -2}"                         # "four five" (last 2 elements, note space)

# Element substring: ${array[n]:start:length}
echo "${arr[3]:0:3}"                         # "thr" (first 3 chars of "three")

# Practical: Process in batches
batch_size=10
total=${#arr[@]}
for ((i=0; i<total; i+=batch_size)); do
    batch=("${arr[@]:i:batch_size}")
    echo "Processing batch: ${batch[*]}"
done

Array Manipulation

hosts=("vault-01" "ise-01" "bind-01")

# Append
hosts+=("kvm-01")                            # Single element
hosts+=("nas-01" "gitea-01")                 # Multiple elements

# Prepend (recreate array)
hosts=("pfsense-01" "${hosts[@]}")

# Remove by index (unset leaves gap)
unset 'hosts[2]'                             # Removes element, index 2 now empty
hosts=("${hosts[@]}")                        # Reindex to close gap

# Remove by value
hosts=("${hosts[@]/ise-01/}")                # Replace with empty (leaves blank)
hosts=("${hosts[@]}")                        # Clean up blanks

# Remove by value (precise)
remove_element() {
    local target="$1"; shift
    local arr=("$@")
    local result=()
    for item in "${arr[@]}"; do
        [[ "$item" != "$target" ]] && result+=("$item")
    done
    echo "${result[@]}"
}
hosts=( $(remove_element "bind-01" "${hosts[@]}") )

# Replace by index
hosts[0]="pfsense-02"

# Replace by pattern (all elements)
hosts=("${hosts[@]/-01/-02}")                # vault-01 → vault-02

Associative Arrays - Key-Value Storage

# Must declare explicitly (bash 4.0+)
declare -A config

# Assignment
config["vault_addr"]="https://vault-01.inside.domusdigitalis.dev:8200"
config["ise_host"]="ise-01.inside.domusdigitalis.dev"
config["dns_server"]="10.50.1.90"

# Bulk assignment
declare -A ports=(
    ["kerberos"]=88
    ["ldap"]=389
    ["ldaps"]=636
    ["dns"]=53
    ["https"]=443
)

# Access
echo "${config[vault_addr]}"                 # Value by key
echo "${!config[@]}"                         # All keys
echo "${config[@]}"                          # All values
echo "${#config[@]}"                         # Number of keys

# Check if key exists
if [[ -v config[vault_addr] ]]; then
    echo "Key exists"
fi

# Default value if missing
echo "${config[missing_key]:-default_value}"

Infrastructure Patterns - Host Inventory

# Host → IP mapping
declare -A hosts=(
    ["vault-01"]="10.50.1.60"
    ["ise-01"]="10.50.1.20"
    ["bind-01"]="10.50.1.90"
    ["kvm-01"]="10.50.1.99"
    ["nas-01"]="10.50.1.70"
    ["home-dc01"]="10.50.1.50"
)

# Iterate with keys and values
for host in "${!hosts[@]}"; do
    ip="${hosts[$host]}"
    echo "Checking $host ($ip)..."
    ping -c1 -W1 "$ip" &>/dev/null && echo "  ✓ UP" || echo "  ✗ DOWN"
done

# Build from command output
declare -A dns_records
while IFS=$'\t' read -r name ip; do
    dns_records["$name"]="$ip"
done < <(dig +short axfr inside.domusdigitalis.dev @bind-01 | awk '/^[a-z].*A\t/ {print $1, $5}')

# Reverse lookup (value → key)
find_host_by_ip() {
    local target_ip="$1"
    for host in "${!hosts[@]}"; do
        [[ "${hosts[$host]}" == "$target_ip" ]] && echo "$host" && return 0
    done
    return 1
}
find_host_by_ip "10.50.1.60"                 # Returns: vault-01

Configuration Management

# Load config file into associative array
declare -A config
load_config() {
    local file="$1"
    while IFS='=' read -r key value; do
        # Skip comments and empty lines
        [[ "$key" =~ ^[[:space:]]*# ]] && continue
        [[ -z "$key" ]] && continue
        # Trim whitespace
        key="${key// /}"
        value="${value#"${value%%[![:space:]]*}"}"
        config["$key"]="$value"
    done < "$file"
}
load_config "/etc/myapp/config.ini"

# Export to environment
for key in "${!config[@]}"; do
    export "${key^^}=${config[$key]}"        # Uppercase key as env var
done

# Merge configs (later wins)
declare -A defaults=(["timeout"]=30 ["retries"]=3 ["verbose"]=false)
declare -A overrides=(["timeout"]=60 ["debug"]=true)

declare -A merged
for key in "${!defaults[@]}"; do merged["$key"]="${defaults[$key]}"; done
for key in "${!overrides[@]}"; do merged["$key"]="${overrides[$key]}"; done

Parallel Processing with Arrays

hosts=("vault-01" "ise-01" "bind-01" "kvm-01" "nas-01")

# Parallel ping check (background jobs)
declare -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"
done
wait

# Parallel with job control (max N concurrent)
max_jobs=5
job_count=0
for host in "${hosts[@]}"; do
    (
        ssh -o ConnectTimeout=5 "$host" "uptime" 2>/dev/null || echo "$host: UNREACHABLE"
    ) &
    ((job_count++))
    if ((job_count >= max_jobs)); then
        wait -n                              # Wait for any one job to finish
        ((job_count--))
    fi
done
wait                                         # Wait for remaining jobs

# xargs parallel (simpler)
printf '%s\n' "${hosts[@]}" | xargs -P5 -I{} ssh {} "hostname; uptime"

Pipeline Integration

# Array from pipeline (WRONG - subshell loses data)
hosts=()
cat /tmp/hostlist.txt | while read -r host; do
    hosts+=("$host")                         # This modifies subshell's array!
done
echo "${#hosts[@]}"                          # Still 0!

# Array from pipeline (CORRECT - process substitution)
hosts=()
while read -r host; do
    hosts+=("$host")
done < <(cat /tmp/hostlist.txt)
echo "${#hosts[@]}"                          # Correct count

# Array from pipeline (CORRECT - mapfile/readarray)
mapfile -t hosts < <(kubectl get nodes -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n')

# Array to pipeline
printf '%s\n' "${hosts[@]}" | sort | uniq

# Filter array through pipeline
mapfile -t active_hosts < <(printf '%s\n' "${hosts[@]}" | grep -v "disabled")

# Transform array elements
mapfile -t fqdns < <(printf '%s\n' "${hosts[@]}" | sed 's/$/.inside.domusdigitalis.dev/')

Real Infrastructure: ISE Session Analysis

# Parse ISE sessions into arrays
declare -a macs users profiles
while IFS=$'\t' read -r mac user profile; do
    macs+=("$mac")
    users+=("$user")
    profiles+=("$profile")
done < <(netapi ise mnt sessions --format json | jq -r '.[] | [.calling_station_id, .user_name, .selected_azn_profiles] | @tsv')

# Build associative array: MAC → User
declare -A mac_to_user
for i in "${!macs[@]}"; do
    mac_to_user["${macs[i]}"]="${users[i]}"
done

# Count sessions per profile
declare -A profile_counts
for profile in "${profiles[@]}"; do
    ((profile_counts["$profile"]++))
done

# Sort by count
for profile in "${!profile_counts[@]}"; do
    echo "${profile_counts[$profile]} $profile"
done | sort -rn | head -10

# Find all MACs for a user
find_macs_by_user() {
    local target_user="$1"
    for mac in "${!mac_to_user[@]}"; do
        [[ "${mac_to_user[$mac]}" == "$target_user" ]] && echo "$mac"
    done
}

Real Infrastructure: Vault Secret Management

# List all secrets at a path into array
mapfile -t secrets < <(vault kv list -format=json kv/infrastructure | jq -r '.[]')

# Read multiple secrets in parallel
declare -A secret_values
for secret in "${secrets[@]}"; do
    (
        value=$(vault kv get -field=password "kv/infrastructure/$secret" 2>/dev/null)
        echo "$secret:$value"
    ) &
done | while IFS=: read -r key val; do
    secret_values["$key"]="$val"
done
wait

# Batch secret operations
hosts=("vault-01" "ise-01" "bind-01")
for host in "${hosts[@]}"; do
    vault kv put "kv/ssh-certs/$host" \
        cert="$(cat "/etc/ssh/certs/${host}.crt")" \
        key="$(cat "/etc/ssh/certs/${host}.key")"
done

Real Infrastructure: Kubernetes Operations

# Get all pod names into array
mapfile -t pods < <(kubectl get pods -n wazuh -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n')

# Build status map
declare -A pod_status
while read -r name status; do
    pod_status["$name"]="$status"
done < <(kubectl get pods -n wazuh -o custom-columns='NAME:.metadata.name,STATUS:.status.phase' --no-headers)

# Filter pods by status
get_pods_by_status() {
    local target_status="$1"
    for pod in "${!pod_status[@]}"; do
        [[ "${pod_status[$pod]}" == "$target_status" ]] && echo "$pod"
    done
}
mapfile -t running_pods < <(get_pods_by_status "Running")

# Execute command on multiple pods
for pod in "${running_pods[@]}"; do
    echo "=== $pod ==="
    kubectl exec -n wazuh "$pod" -- df -h /var 2>/dev/null | tail -1
done

# Resource usage per pod
declare -A pod_cpu pod_mem
while read -r pod cpu mem; do
    pod_cpu["$pod"]="$cpu"
    pod_mem["$pod"]="$mem"
done < <(kubectl top pods -n wazuh --no-headers | awk '{print $1, $2, $3}')

Critical Gotchas

# WRONG: Unquoted expansion splits on whitespace
files=("file one.txt" "file two.txt")
for f in ${files[@]}; do echo "$f"; done     # Prints 4 items!

# CORRECT: Quote the expansion
for f in "${files[@]}"; do echo "$f"; done   # Prints 2 items

# WRONG: for..in with array variable (not expansion)
for f in $files; do echo "$f"; done          # Only first element!

# WRONG: Checking if array is empty
if [ -z "$arr" ]; then                       # Only checks first element
if [ ${#arr[@]} -eq 0 ]; then                # CORRECT: check length

# WRONG: Passing array to function
myfunc arr                                   # Passes string "arr"
myfunc "${arr[@]}"                           # CORRECT: expand array

# WRONG: Returning array from function
myfunc() { echo "${arr[@]}"; }               # Returns string
arr=( $(myfunc) )                            # Breaks on spaces

# CORRECT: Use nameref (bash 4.3+)
myfunc() {
    local -n result=$1                       # Nameref to caller's variable
    result=("one" "two" "three")
}
declare -a myarr
myfunc myarr
echo "${myarr[@]}"

# WRONG: Array index with space
hosts["vault 01"]="10.50.1.60"               # Key has space
echo "${hosts[vault 01]}"                    # Syntax error!
echo "${hosts["vault 01"]}"                  # CORRECT: quote key

# Sparse arrays after unset
arr=(a b c d e)
unset 'arr[2]'
echo "${arr[2]}"                             # Empty (not "d"!)
echo "${!arr[@]}"                            # "0 1 3 4" (index 2 missing)