← retour aux snippets

dotenv: upsert clé idempotent

Ajouter ou mettre à jour une clé dans un .env sans doublons, avec validation et écriture sûre.

bash config #env#dotenv#kv#cli

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 .env sû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 .env ont d’autres règles.
  • l’écriture se fait via fichier temporaire + mv -f pour limiter les corruptions. Ajoutez un flock si plusieurs processus écrivent simultanément.
  • pour des fichiers sourcés par bash, vous pouvez préfixer par export. Pour des applications type dotenv, KEY=VAL suffit.
  • évitez d’imprimer des secrets (pas d’echo $API_TOKEN). Préférez read -s et nettoyez les variables après usage (unset).