Files
multi-agent-mux/.agents/skills/lib_py/delegation/envfile.py
T
GodopuandClaude Opus 5.5 34241d7a65 fix(issue-26): review round 1 - no file key assigns bash's own variables; SKS_QUIESCENT_TRIES; long decimals
R1 (P1): a quoted export still ran code when the key was a bash special
variable (RANDOM=a[$(cmd)] is evaluated arithmetically on assignment).
envfile.RESERVED (bash's own variables, plus BASH_/COMP_/READLINE_
prefixes) is skipped with a line-numbered warning by both loaders; a test
keeps it covering every variable a clean bash defines. The wrapper also
hands SCRIPT_DIR/REPO_ROOT/TARGET_ENV to the parser as environment, so the
file cannot replace the variables it has already set.

R2 (P2): SKS_QUIESCENT_TRIES reached `for ((i < tries))` through a function
argument; it is normalised at the call site (minimum 0). B5 now requires
every integer-looking env default in the shell code to be normalised or
listed in NOT_ARITHMETIC with where it goes, and exercises _pane_quiescent.

R3 (P2): _positive_int strips leading zeros and rejects more than nine
significant digits before int(), like mam_int; 5000-digit inputs no longer
raise in the runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:39:08 +00:00

104 lines
4.1 KiB
Python

"""The one `.mam.env` parser (Issue #26).
Rules: blank lines and `#` comments are skipped; `KEY=VALUE` with the key and
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.
Stdlib only: the delegate-job wrapper runs this file by path
(``envfile.py --shell <path>``) and evals the ``export`` lines it prints, so
values travel over a pipe, never on a command line.
"""
import os
import re
import shlex
import sys
from typing import List, MutableMapping, Tuple
_KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
# Bash's own variables (bash(1) "Shell Variables"). Assigning some runs code
# even from a quoted value (RANDOM/SECONDS/... are evaluated arithmetically,
# so RANDOM='a[$(cmd)]' executes cmd), others steer the shell (IFS, PS4,
# BASH_ENV, SHELLOPTS) or are readonly. A file key never assigns them - in
# either loader. tests/test_issue26_envfile.py checks this covers every
# variable a clean bash defines.
RESERVED = frozenset("""
BASH BASHOPTS BASHPID CDPATH CHILD_MAX COLUMNS COMPREPLY COPROC DIRSTACK EMACS ENV
EPOCHREALTIME EPOCHSECONDS EUID EXECIGNORE FCEDIT FIGNORE FUNCNAME FUNCNEST GLOBIGNORE
GLOBSORT GROUPS HISTCMD HISTCONTROL HISTFILE HISTFILESIZE HISTIGNORE HISTSIZE
HISTTIMEFORMAT HOSTFILE HOSTNAME HOSTTYPE IFS IGNOREEOF INPUTRC INSIDE_EMACS LINENO LINES
MACHTYPE MAIL MAILCHECK MAILPATH MAPFILE OLDPWD OPTARG OPTERR OPTIND OSTYPE PIPESTATUS
POSIXLY_CORRECT PPID PROMPT_COMMAND PROMPT_DIRTRIM PS0 PS1 PS2 PS3 PS4 PWD RANDOM REPLY
SECONDS SHELL SHELLOPTS SHLVL SRANDOM TIMEFORMAT TMOUT UID auto_resume histchars _
""".split())
RESERVED_PREFIXES = ("BASH_", "COMP_", "READLINE_")
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``."""
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__})"]
for n, raw in enumerate(lines, 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
key, sep, val = line.partition("=")
key = key.strip()
if not sep:
warnings.append(f"{path}:{n}: skipped (no '=')")
continue
if not _KEY.fullmatch(key):
warnings.append(f"{path}:{n}: skipped (invalid key)")
continue
if is_reserved(key):
warnings.append(f"{path}:{n}: skipped (reserved shell variable {key})")
continue
val = val.strip()
if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'":
val = val[1:-1]
pairs.append((key, val))
return pairs, warnings
def apply(path: str, environ: MutableMapping[str, str]) -> Tuple[List[Tuple[str, str]], List[str]]:
"""Set each pair not already in ``environ``; return (the pairs set, warnings)."""
pairs, warnings = parse(path)
applied = []
for key, val in pairs:
if key not in environ:
environ[key] = val
applied.append((key, val))
return applied, warnings
def main(argv: List[str]) -> int:
if len(argv) != 2 or argv[0] != "--shell":
print("usage: envfile.py --shell <path>", file=sys.stderr)
return 2
path = argv[1]
if not os.path.isfile(path):
return 0
applied, warnings = apply(path, dict(os.environ))
for w in warnings:
print(f"WARNING: {w}", file=sys.stderr)
for key, val in applied:
print(f"export {key}={shlex.quote(val)}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))