forked from molecule-ai/molecule-core
fix(dotenv): empty value with inline comment was returning the comment
The repo's own .env contains lines like
CONFIGS_DIR= # Path to workspace-configs-templates/...
where the value is empty + an inline comment. The pre-fix parser:
1. v = " # Path to ..."
2. TrimLeft → "# Path to ..."
3. Inline-comment loop looked for " #" or "\t#" — neither matches
because the leading whitespace is gone.
4. Returned the comment text as the value.
Result: os.Setenv("CONFIGS_DIR", "# Path to ...") clobbered the auto-
discovery fallback. The TemplatesHandler then opened the comment as
a directory, ReadDir errored silently, and GET /templates returned
[]. Canvas's Templates panel showed "No templates found in
workspace-configs-templates/" even though 8 valid templates existed
on disk.
Fix: strip leading whitespace from the value FIRST, then run a
position-aware comment scan that treats `#` as a comment marker iff
it's at the start of the (trimmed) value or preceded by whitespace.
A bare `#` mid-value (e.g. `KEY=token#fragment`) still survives.
Quoted-value handling moved above the comment scan so
`KEY="value # not"` keeps the `#` as part of the value — pulled the
quote-detection into the same TrimLeft-then-check shape as the bare
path. The unterminated-quote case still falls through to bare-value
handling.
Three regression tests added covering the exact .env line that
broke (`CONFIGS_DIR= # ...`), spaces-only with comment, and tab-
only with comment.
Verified end-to-end: GET /templates now returns all 8 templates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9a223afba1
commit
4014513b94
@ -144,23 +144,34 @@ func parseDotEnvLine(line string) (string, string, bool) {
|
||||
}
|
||||
k := strings.TrimSpace(line[:eq])
|
||||
v := line[eq+1:]
|
||||
// Quoted value: strip one matched pair of surrounding quotes and
|
||||
// take the contents verbatim (no inline-comment splitting). Matches
|
||||
// the godotenv convention so values with leading/trailing spaces or
|
||||
// `#` survive round-trip.
|
||||
// Trim leading whitespace so a quoted value's opening quote is at
|
||||
// v[0]. The comment-detection loop below then treats the position
|
||||
// after the trim as "start of value" — `KEY= # comment` has its
|
||||
// `#` at the new v[0] (preceded only by whitespace in the source)
|
||||
// and is correctly classified as an empty value followed by a
|
||||
// comment, not as a value of `# comment`.
|
||||
v = strings.TrimLeft(v, " \t")
|
||||
if len(v) >= 2 {
|
||||
first := v[0]
|
||||
if (first == '"' || first == '\'') && v[len(v)-1] == first {
|
||||
return k, v[1 : len(v)-1], true
|
||||
// Quoted value: strip one matched pair of surrounding quotes and
|
||||
// take the contents verbatim (no inline-comment splitting). Must
|
||||
// happen BEFORE comment detection so `KEY="value # not a comment"`
|
||||
// keeps the `#` as part of the value.
|
||||
if len(v) >= 2 && (v[0] == '"' || v[0] == '\'') {
|
||||
quote := v[0]
|
||||
if end := strings.IndexByte(v[1:], quote); end >= 0 {
|
||||
return k, v[1 : 1+end], true
|
||||
}
|
||||
// Unterminated quote — fall through to bare-value handling
|
||||
// (treats the opening quote as a literal char in the value).
|
||||
}
|
||||
// Bare value: strip inline comment introduced by whitespace + `#`.
|
||||
// A bare `#` inside the value (no preceding whitespace) is part of
|
||||
// the value — matches dotenv parsers and lets `KEY=token#fragment`
|
||||
// round-trip.
|
||||
for _, sep := range []string{" #", "\t#"} {
|
||||
if i := strings.Index(v, sep); i >= 0 {
|
||||
// Bare value: strip inline comment. A `#` is a comment marker iff
|
||||
// it's at the start of the (trimmed) value OR is preceded by
|
||||
// whitespace. `KEY=token#fragment` keeps the `#` as part of the
|
||||
// value because v[i-1] is alphanum.
|
||||
for i := 0; i < len(v); i++ {
|
||||
if v[i] != '#' {
|
||||
continue
|
||||
}
|
||||
if i == 0 || v[i-1] == ' ' || v[i-1] == '\t' {
|
||||
v = v[:i]
|
||||
break
|
||||
}
|
||||
|
||||
@ -36,6 +36,18 @@ func TestParseDotEnvLine(t *testing.T) {
|
||||
{in: "FOO=", k: "FOO", v: "", ok: true, comment: "empty value"},
|
||||
{in: "ADMIN_TOKEN=", k: "ADMIN_TOKEN", v: "", ok: true, comment: "empty value (production gate sentinel)"},
|
||||
|
||||
// Regression: the repo's own .env contains lines like
|
||||
// `CONFIGS_DIR= # Path to ...` where the value
|
||||
// is empty + an inline comment. Pre-fix parser stripped leading
|
||||
// whitespace BEFORE detecting the comment, leaving `#` at v[0]
|
||||
// with nothing preceding it, so the inline-comment check missed
|
||||
// it and the comment text was returned as the value. Server
|
||||
// then tried to use the comment as a directory path and template
|
||||
// loading silently failed (GET /templates returned []).
|
||||
{in: "CONFIGS_DIR= # Path to /var/foo (auto-discovered if empty)", k: "CONFIGS_DIR", v: "", ok: true, comment: "empty value with leading whitespace + inline comment"},
|
||||
{in: "FOO= # comment", k: "FOO", v: "", ok: true, comment: "spaces-only value with inline comment"},
|
||||
{in: "FOO=\t# comment", k: "FOO", v: "", ok: true, comment: "tab-only value with inline comment"},
|
||||
|
||||
// `export` prefix: shell-friendly .env files (direnv, .envrc-style)
|
||||
// — the prefix must be stripped, NOT folded into the key.
|
||||
{in: "export FOO=bar", k: "FOO", v: "bar", ok: true, comment: "export prefix stripped"},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user