objectif
Mettre à jour ou injecter une paire KEY=VALUE dans un fichier .env de façon idempotente: sans doublons, en préservant les autres lignes, avec validation de clé et écriture sûre.
code minimal
# usage: upsert_env <fichier> <KEY> <VALUE>
upsert_env() {
local file="$1" key="$2" val="$3"
[[ "$key" =~ ^[A-Z0-9_]+$ ]] || { echo "clé invalide: $key" >&2; return 2; }
[ -e "$file" ] || install -m 0640 /dev/null "$file"
# quoting léger: si espaces/#/quote, envelopper en "..." avec échappements
local qv="$val"
if [[ "$qv" =~ [[:space:]#\"] ]]; then
qv=$(printf '%s' "$qv" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
qv="\"$qv\""
fi
local tmp; tmp="$(mktemp -p "$(dirname "$file")" .env.XXXXXX)" || return 3
awk -v K="$key" -v V="$qv" '
BEGIN{repl=0}
{
if (repl==0 && $0 ~ "^[[:space:]]*(export[[:space:]]+)?"+K"[[:space:]]*=") {
pref=""; if (match($0, /^[[:space:]]*export[[:space:]]+/)) pref="export ";
print pref K "=" V; repl=1; next
}
print
}
END { if (repl==0) print K "=" V }
' "$file" > "$tmp" || { rm -f "$tmp"; return 4; }
# écriture atomique (rename) + droits stables
chmod --reference="$file" "$tmp" 2>/dev/null || chmod 0640 "$tmp"
mv -f "$tmp" "$file"
}
# exemple rapide
upsert_env "/srv/data.pm/.env" "API_URL" "https://api.data.pm"
utilisation
# 1) définir la prod sur data.pm
upsert_env "/srv/data.pm/.env" "APP_ENV" "production"
upsert_env "/srv/data.pm/.env" "API_URL" "https://api.data.pm"
# 2) ajouter un secret lu depuis stdin sans l'imprimer (saisie masquée)
read -r -s -p "Token API: " t; echo
upsert_env "/srv/api.data.pm/.env" "API_TOKEN" "$t"; unset t
# 3) forcer "export KEY=VAL" (pour fichiers sourcés par bash)
upsert_env_export() {
local file="$1" key="$2" val="$3"
upsert_env "$file" "$key" "$val"
sed -i.bak -E "s@^[[:space:]]*(${key})[[:space:]]*=@export \1=@; t; \$a export ${key}=$(grep -E \"^${key}=\" \"$file\" | head -n1 | cut -d= -f2-)@" "$file" 2>/dev/null \
|| awk -v K="$key" 'BEGIN{e=1} $0 ~ "^[[:space:]]*export[[:space:]]+"K"=" {e=0} END{exit e}' "$file" || true
}
upsert_env_export "/srv/jobs/.env" "JOBS_CONCURRENCY" "4"
# 4) batch: plusieurs clés à la suite (idempotent)
f="/srv/api.data.pm/.env"
upsert_env "$f" "LOG_LEVEL" "info"
upsert_env "$f" "FEATURE_X" "true"
upsert_env "$f" "PUBLIC_BASE_URL" "https://data.pm"
# 5) vérifier le résultat (sans révéler les secrets)
grep -E '^(APP_ENV|API_URL|LOG_LEVEL|PUBLIC_BASE_URL)=' "/srv/api.data.pm/.env"
variante(s) utile(s)
# variante stricte: refuser si la clé existe déjà avec une autre valeur (mode "enforce")
upsert_env_strict() {
local file="$1" key="$2" val="$3"
local cur
cur="$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" "$file" 2>/dev/null || true)"
if [ -n "$cur" ] && ! grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=${val}$" <<<"$cur"; then
echo "conflit: ${key} déjà présent: $cur" >&2; return 10
fi
upsert_env "$file" "$key" "$val"
}
# verrouillage simple (concurrence) autour des writes
upsert_env_locked() {
local file="$1"; shift
exec 9>"$file.lock"
flock -n 9 || { echo "lock pris: $file.lock" >&2; return 11; }
upsert_env "$file" "$@"
local rc=$?; rm -f "$file.lock"; return $rc
}
# sauvegarde avant modification (rollback facile)
backup_env() {
local f="$1"; [ -f "$f" ] || return 0
cp -a "$f" "$f.$(date -u +%Y%m%dT%H%M%SZ).bak"
}
backup_env "/srv/data.pm/.env"
upsert_env "/srv/data.pm/.env" "CACHE_TTL" "300"
# lecture sécurisée d'une valeur (sans eval)
get_env() {
local file="$1" key="$2"
awk -v K="$key" '
$0 ~ "^[[:space:]]*(export[[:space:]]+)?"+K"[[:space:]]*=" {
sub(/^[[:space:]]*export[[:space:]]+/,"")
sub(/^[[:space:]]*[^=]+=/,"")
print; exit
}' "$file"
}
get_env "/srv/api.data.pm/.env" "PUBLIC_BASE_URL"
notes
- les clés
.envsûres:^[A-Z0-9_]+$. Évitez les minuscules et caractères spéciaux côté clés. - le quoting appliqué entoure de
"..."si nécessaire (espaces,#,"), avec échappement basique\"et\\. Adaptez si vos parsers.envont d’autres règles. - l’écriture se fait via fichier temporaire +
mv -fpour limiter les corruptions. Ajoutez unflocksi plusieurs processus écrivent simultanément. - pour des fichiers sourcés par bash, vous pouvez préfixer par
export. Pour des applications type dotenv,KEY=VALsuffit. - évitez d’imprimer des secrets (pas d’
echo $API_TOKEN). Préférezread -set nettoyez les variables après usage (unset).