"""Read local environment configuration without evaluating its contents.""" from __future__ import annotations import os import re import sys from pathlib import Path _ASSIGNMENT = re.compile(r"(?:export[ \t]+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)") def load_environment(path: Path, current: dict[str, str]) -> dict[str, str]: """Merge a local .env file with the current environment, which takes priority. Values are literal: shell commands, variable references, backslashes and inline ``#`` characters are never interpreted. Optional surrounding quotes are removed, without interpreting escapes. Keep the project's .env values unquoted (and omit ``export``) for compatibility with Docker ``--env-file``. """ try: contents = path.read_bytes() except FileNotFoundError: return dict(current) values: dict[str, str] = {} for line_number, raw_line in enumerate(contents.split(b"\n"), start=1): # Accept both LF and CRLF, but not embedded line breaks in a value. raw_line = raw_line.removesuffix(b"\r") try: line = raw_line.decode("utf-8") except UnicodeDecodeError: raise ValueError(f"Invalid .env syntax at line {line_number}") from None if "\x00" in line or "\r" in line: raise ValueError(f"Invalid .env syntax at line {line_number}") line = line.lstrip(" \t") if not line or line.startswith("#"): continue match = _ASSIGNMENT.fullmatch(line) if match is None: raise ValueError(f"Invalid .env syntax at line {line_number}") key, value = match.groups() if value.startswith(("'", '"')): quoted_value = value.rstrip(" \t") if len(quoted_value) < 2 or quoted_value[-1] != quoted_value[0]: raise ValueError(f"Invalid .env syntax at line {line_number}") value = quoted_value[1:-1] values[key] = value values.update(current) return values def main(argv: list[str] | None = None) -> int: """Run a command with the project's .env, retaining the caller's directory.""" command = sys.argv[1:] if argv is None else argv if not command or command[0] in ("-h", "--help"): print( "Usage: python3 scripts/env_config.py COMMAND [ARGS...]\n" "Run COMMAND with the project .env; existing environment variables take priority.\n" "The working directory is preserved. From backend, for example:\n" " python3 ../scripts/env_config.py ./mvnw test", file=sys.stderr if not command else sys.stdout, ) return 2 if not command else 0 try: environment = load_environment(Path(__file__).resolve().parents[1] / ".env", dict(os.environ)) except ValueError as error: message = str(error) if re.fullmatch(r"Invalid \.env syntax at line [0-9]+", message) is None: message = "Invalid .env syntax." print(message, file=sys.stderr) return 1 except OSError: print("Unable to read .env.", file=sys.stderr) return 1 try: os.execvpe(command[0], command, environment) except OSError: print("Unable to start command.", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())