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:
Hongming Wang 2026-04-24 21:17:21 -07:00
parent 9a223afba1
commit 4014513b94
2 changed files with 37 additions and 14 deletions

View File

@ -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
}

View File

@ -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"},