dotenvy aborted on the production .env (a quote in an unquoted value makes it
read to the end of the file) and echoed the offending line, password
included. envfile.rs ports python-dotenv 1.1.1 (parser + ${VAR} interpolation,
no override): unparsable statements are skipped with a warning that names only
the line number. Verified against python-dotenv on 614 generated inputs
(testdata/). The .env is loaded before the tokio runtime starts.
install_collector.sh now runs the dry run through systemd-run with the unit's
User, WorkingDirectory and EnvironmentFile, i.e. exactly what the service sees.
123 lines
3.0 KiB
Python
123 lines
3.0 KiB
Python
"""Generate rust/collector/testdata/envfile_python_dotenv.json: python-dotenv's
|
|
own parse + interpolation of tricky .env inputs, used by the Rust envfile
|
|
tests so the collector reads .env exactly as the Python app does.
|
|
|
|
uv run python rust/collector/testdata/gen_envfile_cases.py
|
|
"""
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import random
|
|
from pathlib import Path
|
|
|
|
import dotenv
|
|
from dotenv.main import DotEnv
|
|
|
|
FIXED = [
|
|
"A=abc'def\nB=x\"y\nC=1\n",
|
|
"A=1\nPOSTGRES_PASSWORD='abc\nB=2\n",
|
|
"A=1\nPOSTGRES_PASSWORD= \nB=2\n",
|
|
"POSTGRES_PASSWORD=\"ab'c\nB=2\n",
|
|
" export A = spaced value # comment\nB=a#b\n# c\n\nC= \nD\n",
|
|
"A='x\\'y'\nB=\"l1\\nl2\\t\\\"q\\\"\"\nC='multi\nline'\nD='a\\\\b'\n",
|
|
"A='ab\\'",
|
|
"A='x'y\nB=1\n",
|
|
"HOME_FROM_ENV=file\nA=${HOME_FROM_ENV}/x\nB=${A}-${MISSING:-dflt}-${NOPE}\nC=$A ${A:x}\n",
|
|
"A=1\r\nB=2\r\nA=3\r\n",
|
|
"C= # c\nD=#x\nE= #x\n",
|
|
"A#B=1\nexport\nexportA=2\n'q k'=3\n",
|
|
"\ufeffA=bom\n",
|
|
"A=\u00e9\u0e01\u0e02 # thai\n",
|
|
]
|
|
|
|
KEYS = [
|
|
"A",
|
|
"B",
|
|
"C",
|
|
"D",
|
|
"E",
|
|
"export A",
|
|
"'A B'",
|
|
"HOME_FROM_ENV",
|
|
"#A",
|
|
" A",
|
|
"A B",
|
|
]
|
|
SEPS = ["=", " = ", "= ", "", "\t=\t", "=="]
|
|
VALUE_BITS = [
|
|
"abc",
|
|
"'",
|
|
'"',
|
|
"\\",
|
|
"#",
|
|
" #c",
|
|
"${A}",
|
|
"${B:-d}",
|
|
"${",
|
|
"}",
|
|
" ",
|
|
"\t",
|
|
"'q'",
|
|
'"q"',
|
|
"\\'",
|
|
'\\"',
|
|
"\\n",
|
|
"x y",
|
|
"${HOME_FROM_ENV}",
|
|
"${:-z}",
|
|
"$A",
|
|
"\u0e01",
|
|
]
|
|
EOLS = ["\n", "\r\n", "\r", ""]
|
|
|
|
|
|
def random_case(rng: random.Random) -> str:
|
|
lines = []
|
|
for _ in range(rng.randint(1, 5)):
|
|
kind = rng.random()
|
|
if kind < 0.1:
|
|
lines.append(rng.choice(["", " ", "# comment", " # c ' \""]))
|
|
else:
|
|
value = "".join(rng.choice(VALUE_BITS) for _ in range(rng.randint(0, 4)))
|
|
lines.append(rng.choice(KEYS) + rng.choice(SEPS) + value)
|
|
lines.append(rng.choice(EOLS))
|
|
return "".join(lines)
|
|
|
|
|
|
def expected(content: str):
|
|
values = DotEnv(
|
|
None, stream=io.StringIO(content), interpolate=True, override=False
|
|
).dict()
|
|
return [[k, v] for k, v in values.items()]
|
|
|
|
|
|
def main():
|
|
os.environ["HOME_FROM_ENV"] = "/home/x"
|
|
for k in ("A", "B", "C", "D", "E", "MISSING", "NOPE"):
|
|
os.environ.pop(k, None)
|
|
rng = random.Random(20261018)
|
|
inputs = FIXED + [random_case(rng) for _ in range(600)]
|
|
cases = [{"input": c, "expected": expected(c)} for c in inputs]
|
|
out = Path(__file__).with_name("envfile_python_dotenv.json")
|
|
out.write_text(
|
|
json.dumps(
|
|
{
|
|
"python_dotenv": dotenv.__dict__.get("__version__", "1.1.1"),
|
|
"env": {"HOME_FROM_ENV": "/home/x"},
|
|
"cases": cases,
|
|
},
|
|
ensure_ascii=True,
|
|
indent=0,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
print(f"{len(cases)} cases -> {out}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import logging
|
|
|
|
logging.disable(logging.WARNING) # python-dotenv warns on every bad line
|
|
main()
|