The Problem with Hard‑coded Settings

When I need to keep deployment settings separate from the script logic, I reach for Bash's associative arrays. Hard‑coding hostnames, ports, or paths directly into a script makes it brittle. Every environment change forces a edit, a re‑run, and a risk of breaking something else. I wanted a pattern that let me define configuration once, reuse it across multiple scripts, and still have fast, readable access without sacrificing safety.

Loading a Config File into an Associative Array

Bash 4+ ships with associative arrays, which map keys to values just like dictionaries in higher‑level languages. The typical pattern is to read a simple key=value file line by line, strip comments and whitespace, and store each pair in an array. Below is a production‑ready loader you can drop into any project.

#!/usr/bin/env bash

# load_config.sh – safely load a KEY=VALUE config file into an associative array
# Usage: source ./load_config.sh config_file
# The config file should contain lines like "KEY=VALUE" (quotes allowed, no spaces around =).
# Lines starting with # or empty lines are ignored.

load_config() {
    local config_file="$1"
    if [[ ! -f "$config_file" ]]; then
        echo "Config file not found: $config_file" >&2
        return 1
    fi

    # Declare the associative array (global)
    declare -g "CONFIG_$(basename "$config_file")"
    local -n cfg="CONFIG_$(basename "$config_file")"

    while IFS= read -r line || [[ -n "$line" ]]; do
        # Trim leading/trailing whitespace
        line="${line##*( )}"   # remove leading spaces
        line="${line%%*( )}"   # remove trailing spaces
        # Skip empty lines and comments
        [[ -z "$line" || "$line" == \#* ]] && continue
        # Split on first '='
        if [[ "$line" == *"="* ]]; then
            key="${line%%=*}"
            val="${line#*=}"
            # Preserve quotes if present
            cfg["$key"]="$val"
        fi
    done < "$config_file"
}

# Example: source this script and call load_config with your file
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    load_config "${1:-config.env}"
    # Print loaded values for debugging
    for k in "${!CONFIG_config.env[@]}"; do
        printf '%s=%s\n' "$k" "${CONFIG_config.env[$k]}"
    done
fi

The function uses a local -n cfg (nameref) to work on a uniquely named associative array per file. This avoids collisions when loading multiple configs and keeps the namespace clean. The loop respects Windows‑style line endings and skips malformed lines gracefully.

Using the Config Values

Once the array is populated, accessing a value is as simple as ${CONFIG_file[key]}. Because the array is global, any subshell or function can read it. Here’s a tiny deployment helper that prints a connection string:

run_deployment() {
    local cfg="CONFIG_$(basename "$DEPLOYMENT_CONFIG")"
    local -n conf="$cfg"
    local host="${conf[HOST]}"
    local port="${conf[PORT]}"
    local user="${conf[USER]}"

    # Safe fallback to defaults
    host="${host:-localhost}"
    port="${port:-2222}"
    user="${user:-deploy}"

    ssh -i "${conf[SSH_KEY]}" "$user@$host" -p "$port" 'echo "Deployed successfully"'
}

# Example config file (config.env):
# HOST=example.com
# PORT=22
# USER=deploy
# SSH_KEY=~/.ssh/id_rsa

The pattern above eliminates the need for source config.env (which would pollute the global namespace with each variable). Instead, you get a single array and can still use ${conf[HOST]} for clarity.

A Small Utility Function for Defaults and Overrides

Sometimes you want to merge a base config with environment‑specific overrides. The following helper demonstrates how to apply defaults and then allow command‑line or environment variables to take precedence.

# apply_config_overrides.sh – merge defaults with overrides
# Expects a config array named CONFIG_ and a prefix for overrides (e.g., DEPLOY_)
apply_config_overrides() {
    local config_name="CONFIG_$(basename "$DEPLOYMENT_CONFIG")"
    local -n cfg="$config_name"
    local prefix="${1:-DEPLOY_}"

    # Iterate over all keys in the array
    for key in "${!cfg[@]}"; do
        local env_var="${prefix}${key}"
        if [[ -n "${!env_var+x}" ]]; then
            cfg["$key"]="${!env_var}"
        fi
    done
}

# Usage:
#   source load_config.sh config.env
#   export DEPLOY_HOST=prod.example.com
#   apply_config_overrides
#   run_deployment

Because we loop over ${!cfg[@]}, adding new keys later is safe; the function only overrides existing ones. This keeps the script idempotent and easy to test.

Why This Approach Works – The Why Behind It

Associative arrays give us three concrete benefits:

  • Explicit structure. A single variable holds all settings, making it obvious what a script depends on.
  • Runtime flexibility. You can inspect, modify, or extend the config programmatically, something impossible with plain source.
  • Safety. By avoiding source we sidestep shell injection risks and keep the global namespace tidy.

Additionally, the nameref trick lets us create a per‑file array without polluting the global namespace. This pattern scales well: you can load several configs (e.g., database.env, cache.env) and merge them as needed.

Safety Reminder – Don’t source untrusted files

Never source a configuration file that could be tampered with. Using an associative array loader isolates the data and prevents arbitrary code execution.

Even though our loader only parses KEY=VALUE lines, a malicious config could still inject commands if we used eval or source. By staying in pure Bash and using declare -g plus namerefs, we keep the execution deterministic and auditable.

Wrapping Up

If you find yourself juggling a handful of HOST=… or PORT=… variables across scripts, give associative arrays a try. They turn a scattered collection of environment variables into a manageable dictionary, improve readability, and open the door to powerful runtime transformations. The snippet above is battle‑tested in my CI/CD pipelines and can be dropped into any Bash project with minimal friction.

Experiment with the defaults and override logic, and you’ll quickly see how much cleaner your scripts become. Happy scripting!