Skip to content

Fix variable interpolation in .env files loaded through envFile - #14828

Open
Abdelhadi Salmaoui (abdelhadi703) wants to merge 3 commits into
microsoft:mainfrom
abdelhadi703:fix/envfile-interpolation
Open

Abdelhadi Salmaoui (abdelhadi703) wants to merge 3 commits into
microsoft:mainfrom
abdelhadi703:fix/envfile-interpolation

Conversation

@abdelhadi703

@abdelhadi703 Abdelhadi Salmaoui (abdelhadi703) commented Oct 10, 2026 •

Copy link
Copy Markdown

.env files are supposed to support variable interpolation, but when such a file is
loaded through envFile, the values keep their ${...} references and the variable is
never substituted.

With a file like

USER=admin
EMAIL=${USER}@example.org

EMAIL came out as the literal ${USER}@example.org. Later, when the debug
configuration gets expanded, ${USER} is looked up as a launch.json variable, isn't
found, and prints "Invalid variable reference ${USER} in string". The env var that was
meant to land in the debuggee doesn't.

I made ParsedEnvironmentFile resolve ${VAR} while it parses each line, against the
variables already read from the file plus the process environment. A reference to a name we
don't know is left untouched, so ${workspaceFolder} and the other launch.json variables
keep working exactly as before.

Only a reference the value actually asked to expand is expanded. A single-quoted value and a
\${VAR} reference are literal in dotenv, and both were literal here before this change, so
they stay literal now.

Keeping them literal takes more than emitting them unchanged. This parser's output goes through
expandAllStrings immediately afterwards (configurationProvider.expand), which substitutes
every ${...} it knows and warns about the rest, so a bare ${VAR} either got replaced from
the launch configuration or printed the same "Invalid variable reference" warning the issue is
about. Both literal forms are written back as ${dollar}{VAR} instead -- the one form that
expansion leaves alone and collapses to a plain $ once it is done (expand.ts), so the
debuggee is the only place that sees the literal.

Seven tests were added to the existing ParsedEnvironmentFile suite: a reference to a variable
defined earlier in the same file, a reference coming from the initial environment, an unknown
reference staying literal, a reference resolved from the process environment, the two literal
forms, and a resolved reference still coming out bare.

Testing: ran the ParsedEnvironmentFile suite against the parser alone, compiled with the
repo's TypeScript and a local vscode-nls stand-in (mocha 10 does not start on Node 26, so the
compiled file runs under a short TDD shim). 14 passing with this change.

I also ran that output through the expansion rule from expand.ts, which is what the extension
does next. Both literal forms reach the debuggee as ${USER} with no warning, while
${workspaceFolder} still resolves from the debug configuration. On the previous commit of this
branch the single-quoted value came out as admin@example.org, because the expansion took
${USER} from the launch variables.

The path through extendDebugConfiguration itself needs the extension host and the packages
from the private feed, so it is not covered here.

Fixes #13781.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Interpolation incorrectly expands quoted or escaped literals, and the tests are incomplete and environment-dependent.

2 open findings
What changed in this PR

Adds dotenv-style ${VAR} interpolation for debugger envFile parsing.

Changes:

  • Resolves references from prior entries and the process environment.
  • Preserves unknown references.
  • Adds tests and changelog documentation.
File Description
Extension/​src/​Debugger/​ParsedEnvironmentFile.ts Implements interpolation.
Extension/​test/​scenarios/​SingleRootProject/​tests/​ParsedEnvironmentFile.test.ts Adds interpolation tests.
Extension/​CHANGELOG.md Documents the fix.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Extension/src/Debugger/ParsedEnvironmentFile.ts Outdated
The quotes come off before the references are resolved, so VALUE='${SECRET}' was expanded and
a \${SECRET} reference kept its backslash while losing its meaning. Both are literal in dotenv,
and both were literal before the interpolation existed.

Adds the missing test for the process-environment fallback the changelog advertises.
@abdelhadi703

Abdelhadi Salmaoui (abdelhadi703) commented Oct 13, 2026 •

Copy link
Copy Markdown
Author

Both addressed in 66ea67b.

The quotes came off before the references were resolved, so you are right on both counts: VALUE='${SECRET}' was expanded, and \${SECRET} lost its meaning while keeping its backslash. Both are literal in dotenv, and both were literal here before the interpolation existed, so they stay as written now -- the backslash is dropped, leaving the text ${VAR}. Two tests cover the two forms.

The process-environment fallback has a test now as well: it sets a name of its own, parses a reference to it, and restores the previous value in a finally.

13 passing against the first commit of this branch. The two literal tests fail there -- admin@example.org where ${USER}@example.org is expected for the single-quoted one, and \admin@example.org for the escaped one -- and the other 11 pass either way.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Literal references are exposed to subsequent launch-variable expansion instead of reaching the debuggee unchanged.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment on lines +71 to +74
if (!singleQuoted) {
value = value.replace(/(\\)?\$\{([\w\.\-]+)\}/g, (full: string, escape: string | undefined, name: string): string => {
if (escape) {
return full.substring(1);
Comment thread Extension/CHANGELOG.md Outdated
* Reduce CPU usage when resolving headers in large Windows workspaces.

### Bug Fixes
* Fix `${VAR}` interpolation in `.env` files used by `envFile`: references to variables defined earlier in the same file (or in the OS environment) are now resolved. Single-quoted values and `\${VAR}` are left as written. [#13781](https://github.com/microsoft/vscode-cpptools/issues/13781)
The parser's output goes through expandAllStrings straight after it (configurationProvider.expand),
which substitutes every ${...} it knows and warns about the rest. Writing a literal back as a bare
${VAR} therefore did not keep it literal: the expansion either replaced it from the launch
configuration or reported "Invalid variable reference" -- the very symptom this change set out to
remove. Both literal forms now come out as ${dollar}{VAR}, the one form that expansion leaves alone
and collapses to a plain $ once it is finished.

Drops the changelog entry, since those are written separately before a release.
@abdelhadi703

Copy link
Copy Markdown
Author

You are right, and I had this wrong.

I went looking for a later expansion before writing the previous commit and concluded there was none, because resolveVariables is only applied to envFile and sourceFileMap. I missed that resolveEnvFile is followed by expand(config, folder) two lines further down, and that expandAllStrings walks every environment value. So a bare ${VAR} was never going to survive: it gets taken from the launch variables when one matches, and reported as "Invalid variable reference" when none does -- the exact symptom this change exists to remove. Emitting the literal unchanged only moved the problem.

Both literal forms are now written back as ${dollar}{VAR}, the form that expansion skips and collapses to a plain $ at the end.

Run through the expansion rule from expand.ts, the single-quoted and escaped values both reach the debuggee as ${USER}@example.org with no warning, while ${workspaceFolder} still resolves from the debug configuration. On the previous commit the single-quoted value came out as admin@example.org, straight from the launch variables.

On the wording finding: the changelog entry is gone rather than corrected, for the same reason as the other PR -- those are written separately before a release.

14 tests pass. The path through extendDebugConfiguration itself needs the extension host and the private feed's packages, so it is not covered here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Pull Request

Development

Successfully merging this pull request may close these issues.

[bug] launch.json envFile not capable of variable interpolation

2 participants