From 66f981af817c9217d93b90f119be40ba26e144fb Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 31 Jul 2026 17:34:45 +0200 Subject: [PATCH] feat(lib): add ini_set idempotent INI merge helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openbao's "write config only if absent" pattern would silently skip newly-required keys on a rejoué script. ini_set() merges key by key instead: creates file/section/key as needed, replaces an existing key's value in place, no-ops when already correct, and never touches other sections — so a duplicate key name across sections (e.g. [metrics] ENABLED vs [actions] ENABLED) stays scoped correctly. Atomic write via tmpfile + mv. First OS-agnostic helper in the file, documented as such in the header contract. Refs #18 --- lib/common.sh | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lib/common.sh b/lib/common.sh index 8caad69..605fea5 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -17,6 +17,9 @@ # config`. Reusable as-is by any LXC creator script. # - refresh_os_packages(): Alpine only (apk update && apk upgrade). A # future Debian-based script needs its own apt-get variant. +# - ini_set(): OS-agnostic (plain awk/sed, no OS-specific assumptions). +# Reusable as-is by any script that manages an INI-style config file, +# regardless of the underlying distro. # # Does not set shell options (set -e/-u/-o pipefail): a sourced file must # not impose those on the caller's shell. Both openbao/install.sh and @@ -118,3 +121,69 @@ refresh_os_packages() { log_info "Refreshing Alpine packages..." apk update >/dev/null && apk upgrade >/dev/null } + +# ============================================================ +# #18 - Idempotently set KEY = VALUE in SECTION of an INI-style config file +# (e.g. Gitea's app.ini). Merges key by key rather than overwriting the +# whole file, so a rejoué script can add newly-required keys to an already +# customized config without clobbering it. +# +# Usage: ini_set
+# +# Behavior: +# - Missing file/section/key: created. +# - Key present with a different value: replaced in place. +# - Key present with the same value: no-op (byte-identical output). +# - Other sections/keys: never touched — the match is scoped to the +# given section, so the same key name in a different section (e.g. +# ENABLED in both [metrics] and [actions]) is left alone. +# Comments, blank lines and section order are preserved. Written atomically +# (tmpfile + mv) so an interrupted run can't leave a corrupt config. +# ============================================================ +ini_set() { + local file="$1" section="$2" key="$3" value="$4" + local tmp + + if [[ ! -f "$file" ]]; then + mkdir -p "$(dirname "$file")" + : > "$file" + fi + + tmp=$(mktemp "${file}.tmp.XXXXXX") + + awk -v section="$section" -v key="$key" -v value="$value" ' + /^\[.*\]$/ { + if (in_section && !done) { + printf "%s = %s\n", key, value + done = 1 + } + cur = $0 + gsub(/^\[|\]$/, "", cur) + in_section = (cur == section) + if (in_section) section_found = 1 + print + next + } + { + if (in_section && !done && match($0, "^[ \t]*" key "[ \t]*=")) { + printf "%s = %s\n", key, value + done = 1 + next + } + print + } + END { + if (in_section && !done) { + printf "%s = %s\n", key, value + done = 1 + } + if (!section_found) { + if (NR > 0) print "" + printf "[%s]\n", section + printf "%s = %s\n", key, value + } + } + ' "$file" > "$tmp" + + mv "$tmp" "$file" +}