File
Blob: scripts/config.py
| 1 | """Read single-line dotenv values without evaluating shell expressions.""" |
| 2 | import re |
| 3 | |
| 4 | ENV_ASSIGNMENT = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$") |
| 5 | |
| 6 | |
| 7 | def read_env(path): |
| 8 | values = {} |
| 9 | if path.exists(): |
| 10 | for number, line in enumerate(path.read_text().splitlines(), 1): |
| 11 | if not line.strip() or line.lstrip().startswith("#"): |
| 12 | continue |
| 13 | match = ENV_ASSIGNMENT.fullmatch(line) |
| 14 | if not match: |
| 15 | raise SystemExit(f"{path.name}:{number}: expected NAME=value") |
| 16 | key, raw = match.groups() |
| 17 | if key in values: |
| 18 | raise SystemExit(f"{path.name}:{number}: duplicate {key}") |
| 19 | if raw.startswith(("'", '"', "`")): |
| 20 | quote = raw[0] |
| 21 | end = raw.find(quote, 1) |
| 22 | if end < 0 or (raw[end + 1:].strip() and not raw[end + 1:].lstrip().startswith("#")): |
| 23 | raise SystemExit( |
| 24 | f"{path.name}:{number}: wrap {key} in a quote style absent from its value" |
| 25 | ) |
| 26 | value = raw[1:end] |
| 27 | if quote == '"': |
| 28 | value = value.replace("\\n", "\n").replace("\\r", "\r") |
| 29 | else: |
| 30 | value = raw.split("#", 1)[0].strip() |
| 31 | if any(character in value for character in ("\n", "\r", "\0")): |
| 32 | raise SystemExit(f"{path.name}:{number}: {key} must be a single-line value") |
| 33 | values[key] = value |
| 34 | return values |
| 35 | |
| 36 | |
| 37 | def env_value(key, value): |
| 38 | """Quote a literal value identically for this reader, Node and Wrangler.""" |
| 39 | if any(character in value for character in ("\n", "\r", "\0")): |
| 40 | raise SystemExit(f"{key} must be a single-line value") |
| 41 | for quote in ("'", '"', "`"): |
| 42 | if quote in value: |
| 43 | continue |
| 44 | if quote == '"' and ("\\n" in value or "\\r" in value): |
| 45 | continue |
| 46 | return f"{quote}{value}{quote}" |
| 47 | raise SystemExit(f"{key} needs a value that can be wrapped in one dotenv quote style") |