What you will learn

Load a simple INI-style config file into a Bash associative array (declare -A) safely — without source-ing arbitrary code — so a script can look up settings by key at runtime.

Before you begin

Requires Bash 4+. This approach is for flat key=value files with optional [section] headers; for deeply nested config, use jq/yq against JSON or YAML instead.

1. Parse without executing the file as shell code

#!/usr/bin/env bash
set -Eeuo pipefail

declare -A CONFIG=()

load_config() {
    local file="$1" section="" line key value

    while IFS= read -r line || [[ -n "$line" ]]; do
        line="${line%%#*}"                     # strip trailing comments
        line="${line#"${line%%[![:space:]]*}"}"  # trim leading whitespace
        line="${line%"${line##*[![:space:]]}"}"  # trim trailing whitespace
        [[ -z "$line" ]] && continue

        if [[ "$line" =~ ^\[(.+)\]$ ]]; then
            section="${BASH_REMATCH[1]}"
            continue
        fi

        if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
            key="${BASH_REMATCH[1]}"
            value="${BASH_REMATCH[2]}"
            value="${value%\"}"; value="${value#\"}"   # strip surrounding quotes if present
            CONFIG["${section:+${section}.}${key}"]="$value"
        fi
    done < "$file"
}

Reading line-by-line with a regex match (BASH_REMATCH) means a malicious or malformed line is simply skipped or misparsed, never executed — unlike source, which runs the file as shell code.

2. Sample config and lookup

# app.conf
[database]
host=db01.internal
port=5432

[logging]
level="debug"
load_config "app.conf"

echo "db host: ${CONFIG[database.host]}"
echo "db port: ${CONFIG[database.port]}"
echo "log level: ${CONFIG[logging.level]}"

# Safe lookup with a default for an optional key.
timeout="${CONFIG[database.timeout]:-30}"
echo "timeout (default applied): ${timeout}"

3. Validate required keys exist before using them

require_keys() {
    local missing=0
    for k in "$@"; do
        if [[ -z "${CONFIG[$k]+set}" ]]; then
            echo "missing required config key: ${k}" >&2
            missing=1
        fi
    done
    (( missing == 0 ))
}

require_keys "database.host" "database.port" || exit 1

${CONFIG[$k]+set} tests whether the key exists at all (even with an empty value), which is different from -z "${CONFIG[$k]}" — the latter would also fire on a key that's present but deliberately blank.

Example output

db host: db01.internal
db port: 5432
log level: debug
timeout (default applied): 30

Verify the result

Add a line with no = and a line that's just a comment to the config file and confirm load_config skips both without error, leaving CONFIG populated only with valid entries.

Troubleshooting

If section-prefixed keys aren't found, double-check you're indexing with the literal dot, e.g. CONFIG[database.host] — Bash associative array keys are plain strings, there's no nested structure.

Next steps

Use this to back multi-env-deploy-orchestrator's environment files with sections, or any script that currently hardcodes settings inline.