2626ADDITIONAL namespace-`env` keys keyed on the bare var name, alongside the
2727normal namespace-`yaml` dotted-path ones -- dual-minting is intentional
2828(so `os.environ`/`os.getenv` reads bind to compose/k8s-declared vars too),
29- never deduped away. Ids never collide with the plain yaml mint: the env
30- mint's key is always the bare var name while the yaml mint's key always
31- carries the full dotted path, so the two differ by construction for every
32- shape this pass recognizes. This pass is shape-based, not filename/role
33- gated -- any yaml artifact whose content happens to match mints the extra
34- keys, matching this module's general overlay posture (permissive, never a
35- schema validator).
29+ never deduped away. The `key` FIELD is always the bare var name (matching
30+ `env` namespace's exact-match resolution semantics), but the `id` cannot
31+ reuse that bare name unqualified: a TOP-LEVEL yaml key sharing the same
32+ name as a recognized env var (e.g. a document with both a bare
33+ `COMPOSE_ONLY_KEY:` entry and a `services.web.environment.COMPOSE_ONLY_KEY`
34+ one) would otherwise collide with the plain yaml mint's own bare-key id --
35+ the yaml mint's key is USUALLY a longer dotted path that can't collide, but
36+ not always (a top-level leaf's dotted path IS just its bare name). Same fix
37+ as the dockerfile ARG case: the env-dual-mint's id is disambiguated with an
38+ internal `env.` prefix (`_build_keys`'s `id_key`), the `key` field itself
39+ unaffected. This pass is shape-based, not filename/role gated -- any yaml
40+ artifact whose content happens to match mints the extra keys, matching this
41+ module's general overlay posture (permissive, never a schema validator).
3642
3743Span precision differs by shape: env/properties/ini/dockerfile are
3844line-oriented, so the parse itself knows the exact defining line. yaml/
@@ -231,18 +237,30 @@ def _join_continuations(lines: List[str], start_i: int) -> Tuple[str, int]:
231237
232238
233239def _split_ws_respecting_quotes (s : str ) -> List [str ]:
234- """Whitespace-split `s`, except inside a matching `'`/`"` span (a quoted
235- value may contain spaces) -- quote characters stay IN the returned
236- tokens, stripped afterward by `_env_value` so there is one quote-
237- stripping implementation, not two."""
240+ r"""Whitespace-split `s`, except inside a matching `'`/`"` span (a quoted
241+ value may contain spaces) or right after an unquoted `\` -- a backslash
242+ escapes the next character (`\ ` keeps a literal space in the token
243+ instead of splitting there, `\\` collapses to one literal backslash),
244+ mirroring Docker's own shell-style ENV splitting (moby's `Rex\ The\
245+ Dog` example). A dangling trailing `\` with nothing to escape is kept
246+ literally rather than raising -- a real trailing continuation backslash
247+ is already stripped upstream by `_join_continuations`, so this is only
248+ a defensive fallback. Quote characters stay IN the returned tokens,
249+ stripped afterward by `_env_value` so there is one quote-stripping
250+ implementation, not two."""
238251 tokens : List [str ] = []
239252 buf : List [str ] = []
240253 quote : Optional [str ] = None
241- for ch in s :
254+ i , n = 0 , len (s )
255+ while i < n :
256+ ch = s [i ]
242257 if quote :
243258 buf .append (ch )
244259 if ch == quote :
245260 quote = None
261+ elif ch == "\\ " :
262+ i += 1
263+ buf .append (s [i ] if i < n else ch )
246264 elif ch in "'\" " :
247265 quote = ch
248266 buf .append (ch )
@@ -252,6 +270,7 @@ def _split_ws_respecting_quotes(s: str) -> List[str]:
252270 buf = []
253271 else :
254272 buf .append (ch )
273+ i += 1
255274 if buf :
256275 tokens .append ("" .join (buf ))
257276 return tokens
@@ -262,8 +281,12 @@ def _dockerfile_env_entries(text: str, lines: List[str]) -> List[_Entry]:
262281 `ENV a=1 b=2`, and the legacy single-key `ENV K v` space form (Docker's
263282 own disambiguation rule: the token right after `ENV` decides the form --
264283 a `=` in it means one-or-more `key=value` pairs; no `=` means the
265- legacy form, where the key is the first word and the REST of the line,
266- verbatim, is the value)."""
284+ legacy form, where the key is the first word and the REST of the line
285+ is the value). The legacy form's value is taken VERBATIM -- unlike the
286+ `key=value` form, real Docker does no quote processing there at all
287+ (moby's `parseNameVal`), so `ENV NAME "John Doe"` keeps its quotes; the
288+ key/value separator is general whitespace (a tab is as legal as a
289+ space), not a literal `" "`."""
267290 out : List [_Entry ] = []
268291 i , n = 0 , len (lines )
269292 while i < n :
@@ -282,9 +305,9 @@ def _dockerfile_env_entries(text: str, lines: List[str]) -> List[_Entry]:
282305 if sep and _ENV_KEY_NAME .match (key ):
283306 out .append ((key , _env_value (raw_val ), span ))
284307 else :
285- key , sep , raw_val = rest .partition ( " " )
286- if sep and _ENV_KEY_NAME .match (key ):
287- out .append ((key , _env_value ( raw_val . strip ()) , span ))
308+ parts = rest .split ( None , 1 )
309+ if len ( parts ) == 2 and _ENV_KEY_NAME .match (parts [ 0 ] ):
310+ out .append ((parts [ 0 ], parts [ 1 ] , span ))
288311 i += 1
289312 return out
290313
@@ -454,10 +477,14 @@ def _build_keys(
454477 bool/None that needs that coercion.
455478
456479 `id_key` remaps `dotted_key` for ID CONSTRUCTION only -- the `.key` FIELD
457- always stays the bare `dotted_key`. Used solely so a Dockerfile ARG's id
458- can't collide with an ENV of the same name minting the same bare-name id
459- in the "env" namespace (`ARG X` then `ENV X=$X` is a common promotion
460- idiom); every other namespace omits it, preserving today's id shape."""
480+ always stays the bare `dotted_key`. Two call sites need it, both to keep
481+ a bare-name mint from colliding with another mint that happens to use
482+ the same bare name for its OWN id: a Dockerfile ARG's id (`ARG X` then
483+ `ENV X=$X` is a common promotion idiom -- both would otherwise mint id
484+ `.../@key/X`), and a yaml artifact's compose/k8s env-dual-mint id (a
485+ top-level yaml key sharing a name with a recognized env var would
486+ otherwise collide with the plain yaml mint's own bare-key id). Every
487+ other namespace omits it, preserving today's id shape."""
461488 coalesced : Dict [str , Tuple [object , Optional [Span ]]] = {}
462489 for dotted_key , value , span in entries :
463490 coalesced [dotted_key ] = (value , span )
@@ -545,7 +572,10 @@ def extract_config_keys(
545572 ]
546573 keys = (
547574 _build_keys (artifact .id , "yaml" , _parse_yaml (full_text , lines ), capture_value )
548- + _build_keys (artifact .id , "env" , env_entries , capture_value )
575+ + _build_keys (
576+ artifact .id , "env" , env_entries , capture_value ,
577+ id_key = lambda k : f"env.{ k } " ,
578+ )
549579 )
550580 else :
551581 parser = _NAMESPACE_PARSERS .get (artifact .format )
0 commit comments