j139 dotenv: strip trailing inline comments and warn on non-token values
A `#` preceded by whitespace and outside quotes now ends the value, so `TR_DEBUG_DRAW=0 # hides the grid` resolves to `0` instead of the whole tail. Values that are still not plain tokens (whitespace, `#`, an unclosed quote) get one `[dotenv] WARNING` line naming file, key, raw value and the fact that the reader falls back to its DEFAULT, instead of being applied silently. Guard test 29 -> 47 checks.
This commit is contained in:
@@ -23,6 +23,23 @@
|
||||
##
|
||||
## The boot report (env_report.nim) reads the resolved path/source and the set
|
||||
## of keys that came from the file, so it can label values `(source: .env)`.
|
||||
##
|
||||
## INLINE COMMENTS (j139): a `#` preceded by WHITESPACE and not inside quotes
|
||||
## starts a comment that runs to the end of the line. Rule, in order, on the
|
||||
## text after the `=`:
|
||||
## * the value is scanned left to right, tracking single/double quotes; the
|
||||
## first unquoted `#` that is either at position 0 or preceded by
|
||||
## whitespace ends the value (everything from there is dropped, then the
|
||||
## value is trimmed again);
|
||||
## * a `#` INSIDE quotes is data (`"a # b"` -> `a # b`);
|
||||
## * a `#` with no whitespace before it is data (`value#tag` stays), because
|
||||
## a `#` glued to the value is far more likely to be part of a token than
|
||||
## a comment, and guessing wrong there would silently corrupt data;
|
||||
## * `KEY=# comment` yields the empty value, not an error.
|
||||
##
|
||||
## Because readers fall back to their DEFAULT on any value they do not
|
||||
## recognise, a value that still looks junk after parsing is warned about at
|
||||
## load time (one line per key) instead of being applied silently.
|
||||
|
||||
import std/[os, strutils, sets]
|
||||
|
||||
@@ -97,6 +114,32 @@ proc chooseEnvFile*(flagValue, envValue, cwdCandidate, exeCandidate: string): En
|
||||
|
||||
# ── parsing ──────────────────────────────────────────────────────────────────
|
||||
|
||||
proc stripInlineComment*(raw: string): string =
|
||||
## Pure, exported for the guard test: drop a trailing ` # comment` that is
|
||||
## not inside quotes. See the module doc comment for the full rule.
|
||||
var quote = ' '
|
||||
for i, c in raw:
|
||||
if c == '"' or c == '\'':
|
||||
if quote == ' ': quote = c
|
||||
elif quote == c: quote = ' '
|
||||
elif c == '#' and quote == ' ' and (i == 0 or raw[i - 1] in Whitespace):
|
||||
return raw[0 ..< i]
|
||||
return raw
|
||||
|
||||
proc looksJunky*(value: string): bool =
|
||||
## True when a value is NOT a plain token: it still contains whitespace or a
|
||||
## `#`, or a quote that was never closed. Legitimate values (comma lists like
|
||||
## `1.0,1.25`, paths with `/`, `both`, `off`, `0`) are never flagged.
|
||||
if value.len == 0: return false
|
||||
if value.contains('#'): return true
|
||||
var dq = 0
|
||||
var sq = 0
|
||||
for c in value:
|
||||
if c == '"': inc dq
|
||||
elif c == '\'': inc sq
|
||||
elif c in Whitespace: return true
|
||||
dq mod 2 == 1 or sq mod 2 == 1
|
||||
|
||||
proc parseEnvFileContent*(content, filename: string): seq[EnvEntry] =
|
||||
## Parse the usual .env shape: one KEY=VALUE per line, blank lines and `#`
|
||||
## comments skipped, a leading `export ` tolerated, surrounding single or
|
||||
@@ -123,7 +166,7 @@ proc parseEnvFileContent*(content, filename: string): seq[EnvEntry] =
|
||||
if key.len == 0:
|
||||
raise newException(ValueError,
|
||||
filename & ":" & $lineno & ": empty key in: " & stripped)
|
||||
var value = body[eq + 1 .. ^1].strip()
|
||||
var value = stripInlineComment(body[eq + 1 .. ^1]).strip()
|
||||
if value.len >= 2 and
|
||||
((value[0] == '"' and value[^1] == '"') or
|
||||
(value[0] == '\'' and value[^1] == '\'')):
|
||||
@@ -132,6 +175,18 @@ proc parseEnvFileContent*(content, filename: string): seq[EnvEntry] =
|
||||
|
||||
# ── applying ─────────────────────────────────────────────────────────────────
|
||||
|
||||
proc junkWarnings*(entries: seq[EnvEntry], path: string): seq[string] =
|
||||
## The one-line-per-key warnings for values that are not plain tokens. Pure
|
||||
## (no printing) so the guard test can pin the text; `applyEnvFile` prints
|
||||
## exactly these lines. An offending value is still applied verbatim, but any
|
||||
## reader that does not recognise it falls back to its DEFAULT — the message
|
||||
## says so and tells the user to move the comment onto its own line.
|
||||
for e in entries:
|
||||
if looksJunky(e.value):
|
||||
result.add "[dotenv] WARNING: " & path & ": " & e.key &
|
||||
" raw value looks wrong: \"" & e.value & "\"" &
|
||||
" - the reader will fall back to its DEFAULT; move the comment onto its own line"
|
||||
|
||||
proc applyEnvFile*(path: string): seq[EnvConflict] =
|
||||
## Read `path`, apply every entry with putEnv (the FILE WINS over the real
|
||||
## environment), remember the from-file keys, and return the keys whose file
|
||||
@@ -145,6 +200,10 @@ proc applyEnvFile*(path: string): seq[EnvConflict] =
|
||||
result.add EnvConflict(key: e.key, fileValue: e.value, shellValue: shell)
|
||||
putEnv(e.key, e.value)
|
||||
gEnvFileKeys.incl e.key
|
||||
# j139 safety net: a value that is not a plain token will be rejected by the
|
||||
# reader (which then falls back to its DEFAULT, silently). Say so, once.
|
||||
for w in junkWarnings(entries, path):
|
||||
stderr.writeLine w
|
||||
|
||||
proc loadEnvFile*(choice: EnvFileChoice): seq[EnvConflict] =
|
||||
## Apply the chosen file. A missing EXPLICIT file raises EnvFileError; a
|
||||
|
||||
Reference in New Issue
Block a user