"""读写实例目录 .env(行级 upsert,原子落盘).""" from __future__ import annotations import os import re import tempfile from typing import Optional _KEY_LINE = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$") def parse_env_lines(text: str) -> list[str]: return text.replace("\r\n", "\n").replace("\r", "\n").splitlines() def read_env_lines(path: str) -> list[str]: if not os.path.isfile(path): return [] with open(path, "r", encoding="utf-8", errors="ignore") as f: return parse_env_lines(f.read()) def env_get(lines: list[str], key: str) -> Optional[str]: for line in lines: m = _KEY_LINE.match(line) if m and m.group(2) == key: raw = m.group(3).strip() if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")): return raw[1:-1] return raw return None def env_get_all(lines: list[str]) -> dict[str, str]: out: dict[str, str] = {} for line in lines: m = _KEY_LINE.match(line) if m: key = m.group(2) raw = m.group(3).strip() if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")): out[key] = raw[1:-1] else: out[key] = raw return out def upsert_env_line(lines: list[str], key: str, value: str) -> list[str]: pat = re.compile(r"^\s*" + re.escape(key) + r"\s*=") out: list[str] = [] replaced = False safe = value if value is not None else "" if any(c in safe for c in (' ', '#', '"', "'")): safe = '"' + safe.replace("\\", "\\\\").replace('"', '\\"') + '"' new_line = f"{key}={safe}" for line in lines: if pat.match(line): if not replaced: out.append(new_line) replaced = True continue out.append(line) if not replaced: if out and out[-1].strip(): out.append("") out.append(new_line) return out def write_env_lines_atomic(path: str, lines: list[str]) -> None: directory = os.path.dirname(os.path.abspath(path)) or "." os.makedirs(directory, exist_ok=True) fd, tmp = tempfile.mkstemp(prefix=".env.", dir=directory, text=True) try: with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(lines)) if lines: f.write("\n") os.replace(tmp, path) finally: if os.path.exists(tmp): try: os.remove(tmp) except OSError: pass def apply_env_updates(path: str, updates: dict[str, str]) -> list[str]: lines = read_env_lines(path) changed: list[str] = [] for key, value in updates.items(): if value is None: continue old = env_get(lines, key) if old == value: continue lines = upsert_env_line(lines, key, value) changed.append(key) if changed: write_env_lines_atomic(path, lines) return changed def load_env_file_into_environ(path: str) -> None: if not os.path.exists(path): return with open(path, "r", encoding="utf-8", errors="ignore") as f: text = f.read() if text.startswith("\ufeff"): text = text[1:] for line in parse_env_lines(text): s = line.strip() if not s or s.startswith("#"): continue if "=" not in s: continue k, _, v = s.partition("=") clean_key = k.strip() clean_val = v.strip().strip('"').strip("'") if clean_key: os.environ[clean_key] = clean_val