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)