#!/bin/bash # ============================================================================================== # conf_upgrade.sh — Merge new conf structure into an existing conf while preserving user values. # ============================================================================================== # # Usage: # conf_upgrade.sh --template --target [--dry-run] [--backup] # # What it does: # • Keys in template only → ADDED with template's placeholder/default — user fills in once # • Keys in target only → REMOVED — deprecated in new version # • Keys in both → KEPT — target's value always preserved, template's ignored # • Comments / blank lines → always from template (structure follows new version) # # Supports all conf variable patterns: # Simple scalar: KEY="value" # Indexed array: KEY=(\n "item"\n) # Associative array: declare -A KEY=(\n ["k"]="v"\n) # # Options: # --template New version conf file (source of structure and defaults) # --target Existing user conf (source of real values — preserved) # --dry-run Show what would change without writing # --backup Write a .bak copy of target before modifying # # ============================================================================================== set -uo pipefail # ── Arguments ──────────────────────────────────────────────────────────────────────────────── TEMPLATE="" TARGET="" DRY_RUN=false BACKUP=false while [[ $# -gt 0 ]]; do case "$1" in --template) TEMPLATE="$2"; shift 2 ;; --target) TARGET="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; --backup) BACKUP=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done [[ -z "$TEMPLATE" ]] && { echo "Error: --template required" >&2; exit 1; } [[ -z "$TARGET" ]] && { echo "Error: --target required" >&2; exit 1; } [[ -f "$TEMPLATE" ]] || { echo "Error: template not found: $TEMPLATE" >&2; exit 1; } [[ -f "$TARGET" ]] || { echo "Error: target not found: $TARGET" >&2; exit 1; } # ── Parse target → KEY → full definition block ─────────────────────────────────────────────── declare -A HOST_MAP # KEY → complete definition line(s) from user's conf _parse_target() { local in_block=false cur_key="" cur_block="" line while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$in_block" == true ]]; then cur_block+="$line"$'\n' # Closing ) — optional trailing whitespace and comment if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then HOST_MAP["$cur_key"]="$cur_block" in_block=false; cur_key=""; cur_block="" fi else # declare -A KEY=( if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n' # KEY=( elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n' # KEY=value (simple scalar) elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then HOST_MAP["${BASH_REMATCH[1]}"]="$line"$'\n' fi fi done < "$TARGET" } # ── Walk template — collect stats (must run in current shell so arrays persist) ────────────── declare -a ADDED=() KEPT=() REMOVED=() declare -A TMPL_SEEN=() _collect_stats() { local in_block=false cur_key="" line while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$in_block" == true ]]; then if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then in_block=false TMPL_SEEN["$cur_key"]=1 if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then KEPT+=("$cur_key") else ADDED+=("$cur_key"); fi cur_key="" fi else if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then cur_key="${BASH_REMATCH[1]}"; in_block=true elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then cur_key="${BASH_REMATCH[1]}"; in_block=true elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then local k="${BASH_REMATCH[1]}" TMPL_SEEN["$k"]=1 if [[ -n "${HOST_MAP[$k]+_}" ]]; then KEPT+=("$k") else ADDED+=("$k"); fi fi fi done < "$TEMPLATE" for key in "${!HOST_MAP[@]}"; do [[ -z "${TMPL_SEEN[$key]+_}" ]] && REMOVED+=("$key") done } # ── Walk template — write merged output ────────────────────────────────────────────────────── # Runs in a subshell (stdout redirected) — array mutations are intentionally discarded here. _write_merged() { local in_block=false cur_key="" cur_block="" line while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$in_block" == true ]]; then cur_block+="$line"$'\n' if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then in_block=false if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then printf '%s' "${HOST_MAP[$cur_key]}" else printf '%s' "$cur_block"; fi cur_key=""; cur_block="" fi else if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n' elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n' elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then local k="${BASH_REMATCH[1]}" if [[ -n "${HOST_MAP[$k]+_}" ]]; then printf '%s' "${HOST_MAP[$k]}" else printf '%s\n' "$line"; fi else printf '%s\n' "$line" fi fi done < "$TEMPLATE" } # ── Run ─────────────────────────────────────────────────────────────────────────────────────── _parse_target _collect_stats # ── Report ──────────────────────────────────────────────────────────────────────────────────── TARGET_NAME="$(basename "$TARGET")" echo "" echo "── conf_upgrade: $TARGET_NAME ──────────────────────────────────────────" if [[ ${#ADDED[@]} -gt 0 ]]; then echo " ADDED (new — fill in your values where needed):" for k in "${ADDED[@]}"; do echo " + $k"; done fi if [[ ${#REMOVED[@]} -gt 0 ]]; then echo " REMOVED (deprecated — no longer in this version):" for k in "${REMOVED[@]}"; do echo " - $k"; done fi echo " KEPT ${#KEPT[@]} existing vars — your values preserved" if [[ ${#ADDED[@]} -eq 0 && ${#REMOVED[@]} -eq 0 ]]; then echo " Already up to date — no changes needed." echo "────────────────────────────────────────────────────────────────────────" echo "" exit 0 fi echo "────────────────────────────────────────────────────────────────────────" echo "" # ── Apply ───────────────────────────────────────────────────────────────────────────────────── if [[ "$DRY_RUN" == true ]]; then echo "(dry-run — no changes written)" exit 0 fi TMPOUT="$(mktemp)" trap 'rm -f "$TMPOUT"' EXIT _write_merged > "$TMPOUT" if [[ "$BACKUP" == true ]]; then cp "$TARGET" "${TARGET}.bak" echo "Backup: ${TARGET}.bak" fi cp "$TMPOUT" "$TARGET" echo "Updated: $TARGET"