feat(lib): helper ini_set idempotent pour merge de fichiers INI #24

Closed
Damien wants to merge 3 commits from feat/lib-ini-set into main
+107
View File
@@ -17,6 +17,9 @@
# config`. Reusable as-is by any LXC creator script. # config`. Reusable as-is by any LXC creator script.
# - refresh_os_packages(): Alpine only (apk update && apk upgrade). A # - refresh_os_packages(): Alpine only (apk update && apk upgrade). A
# future Debian-based script needs its own apt-get variant. # 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 # 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 # not impose those on the caller's shell. Both openbao/install.sh and
@@ -118,3 +121,107 @@ refresh_os_packages() {
log_info "Refreshing Alpine packages..." log_info "Refreshing Alpine packages..."
apk update >/dev/null && apk upgrade >/dev/null 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 <file> <section> <key> <value>
#
# 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")
# mktemp defaults to 0600 root:root, which would silently lock the
# service account that owns $file (e.g. gitea:www-data on Gitea's
# app.ini) out of the config this function just wrote. Carry the
# original file's mode/ownership onto the replacement before it lands.
# `stat -c` works identically on GNU coreutils and BusyBox.
chmod "$(stat -c '%a' "$file")" "$tmp" 2>/dev/null || true
chown "$(stat -c '%u:%g' "$file")" "$tmp" 2>/dev/null || true
# awk writes into $tmp regardless of its own exit status — a mid-stream
# death (OOM, signal, an exotic value tripping the regex) would still
# leave a truncated-but-nonempty $tmp for `mv` to install over $file.
# Gate the mv on awk's exit code so a failure leaves the original config
# untouched instead of silently destroying it.
if ! awk -v section="$section" -v key="$key" -v value="$value" '
# Blank lines are buffered rather than printed immediately while a
# section is still awaiting insertion: without this, a key inserted
# right before the next section header lands *after* that section'"'"'s
# trailing blank line(s) instead of before them. Flushed as soon as
# either a non-blank line or the insertion itself happens, so this
# never reorders anything except relative to that pending insertion.
function flush_blanks() {
while (blank_count > 0) { print ""; blank_count-- }
}
/^\[.*\]$/ {
if (in_section && !done) {
printf "%s = %s\n", key, value
done = 1
}
flush_blanks()
cur = $0
gsub(/^\[|\]$/, "", cur)
in_section = (cur == section)
if (in_section) section_found = 1
print
next
}
{
if (in_section && !done) {
if (match($0, "^[ \t]*" key "[ \t]*=")) {
flush_blanks()
printf "%s = %s\n", key, value
done = 1
next
}
if ($0 ~ /^[ \t]*$/) {
blank_count++
next
}
flush_blanks()
print
next
}
print
}
END {
if (in_section && !done) {
printf "%s = %s\n", key, value
done = 1
}
flush_blanks()
if (!section_found) {
if (NR > 0) print ""
printf "[%s]\n", section
printf "%s = %s\n", key, value
}
}
' "$file" > "$tmp"; then
rm -f "$tmp"
log_error "ini_set: awk failed on ${file} (${section}.${key}), config left untouched."
return 1
fi
mv "$tmp" "$file"
}