fix(issue-26): review - an unreadable env file is an error, NUL lines are skipped, integers are ASCII in every locale, empty flags are refused
Merge-workspace review of PR #43 (codex R1-R4, claude R1/R2).
- R1: envfile.parse raises EnvFileError when the file exists but cannot be
read or decoded as a whole; `--shell` prints the cause and exits 1, so the
wrapper stops (rc 1, as before #26) instead of running on defaults, and
mqtt_common's entry points stop on the same error. A bad single line is
still a warning. The wrapper's message no longer claims a read failure
when the cause is elsewhere ("cannot load ... see the line above").
- R2: lib.sh mam_int spells out the ASCII blanks and digits instead of
[[:space:]]/[0-9], which follow the locale: U+2003 30 U+2003 was 30 in the
wrapper and invalid in the runner under en_US.UTF-8. The test oracle
_expected uses the same ASCII rule.
- R3: an integer flag given explicitly with an empty value is refused like
any other invalid value; only an absent flag keeps its default.
- R4: a line holding a NUL byte is skipped with a warning by both loaders;
before, the wrapper read "leftright" and the Python loader died.
- .mam.env.example states the format: no expansion, `export` and bash's own
variables skipped, a trailing `# ...` is part of the value, the environment
wins, an unreadable file is an error.
Tests: unreadable (0000) and invalid-UTF-8 files stop the wrapper and the
Python loader; a NUL line is skipped by both; Unicode blanks/digits are
invalid for mam_int, _positive_int, the wrapper and the runner under C and
en_US.UTF-8; explicit empty values for the five integer flags are refused.
All of them fail on b58edac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
b58edace11
commit
0eec795cd1
7 files changed
+128
-19
No files matched your search
@@ -5,8 +5,11 @@ the value stripped; one matching pair of surrounding quotes is removed; no
|
||||
shell interpretation (`$(...)`, `${...}` and backticks stay literal). A key
|
||||
must match ``^[A-Za-z_][A-Za-z0-9_]*$`` and not be one of bash's own variables
|
||||
(RESERVED); a line that breaks either, or that has no `=`, is skipped with a
|
||||
warning naming the line (never the value — the file holds secrets).
|
||||
Variables already in the environment win over the file.
|
||||
warning naming the line (never the value — the file holds secrets). A line
|
||||
holding a NUL byte is skipped the same way (no environment can carry it).
|
||||
Variables already in the environment win over the file. A file that exists
|
||||
but cannot be read or decoded as a whole is an error (EnvFileError; the
|
||||
``--shell`` command exits 1), not an empty configuration.
|
||||
|
||||
Stdlib only: the delegate-job wrapper runs this file by path
|
||||
(``envfile.py --shell <path>``) and evals the ``export`` lines it prints, so
|
||||
@@ -38,23 +41,31 @@ RESERVED = frozenset("""
|
||||
RESERVED_PREFIXES = ("BASH_", "COMP_", "READLINE_")
|
||||
|
||||
|
||||
class EnvFileError(RuntimeError):
|
||||
"""The env file exists but cannot be read or decoded as a whole."""
|
||||
|
||||
|
||||
def is_reserved(key: str) -> bool:
|
||||
return key in RESERVED or key.startswith(RESERVED_PREFIXES)
|
||||
|
||||
|
||||
def parse(path: str) -> Tuple[List[Tuple[str, str]], List[str]]:
|
||||
"""Return ([(key, value)], [warning]) for the file at ``path``."""
|
||||
"""Return ([(key, value)], [warning]) for the file at ``path``; raise
|
||||
EnvFileError when the file cannot be read or decoded as a whole."""
|
||||
pairs: List[Tuple[str, str]] = []
|
||||
warnings: List[str] = []
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
return pairs, [f"{path}: cannot read ({type(e).__name__})"]
|
||||
raise EnvFileError(f"{path}: cannot read ({type(e).__name__})") from None
|
||||
for n, raw in enumerate(lines, 1):
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "\0" in line:
|
||||
warnings.append(f"{path}:{n}: skipped (NUL byte)")
|
||||
continue
|
||||
key, sep, val = line.partition("=")
|
||||
key = key.strip()
|
||||
if not sep:
|
||||
@@ -91,7 +102,11 @@ def main(argv: List[str]) -> int:
|
||||
path = argv[1]
|
||||
if not os.path.isfile(path):
|
||||
return 0
|
||||
applied, warnings = apply(path, dict(os.environ))
|
||||
try:
|
||||
applied, warnings = apply(path, dict(os.environ))
|
||||
except EnvFileError as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
return 1
|
||||
for w in warnings:
|
||||
print(f"WARNING: {w}", file=sys.stderr)
|
||||
for key, val in applied:
|
||||
|
||||
Reference in new issue
Block a user