diff --git a/Extension/CHANGELOG.md b/Extension/CHANGELOG.md index b8c8b62db..dfdaabcdc 100644 --- a/Extension/CHANGELOG.md +++ b/Extension/CHANGELOG.md @@ -15,6 +15,7 @@ * 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. [#13781](https://github.com/microsoft/vscode-cpptools/issues/13781) * Fix 'Go to Declaration' and 'Go to Definition' returning the current location instead of an available counterpart. [#14509](https://github.com/microsoft/vscode-cpptools/issues/14509) * Update MIEngine to fix debugger failures with synthetic GDB stack frames. [#14741](https://github.com/microsoft/vscode-cpptools/issues/14741) * Thanks for the contribution. [@tzcnt](https://github.com/tzcnt) [PR MIEngine#1605](https://github.com/microsoft/MIEngine/pull/1605) diff --git a/Extension/src/Debugger/ParsedEnvironmentFile.ts b/Extension/src/Debugger/ParsedEnvironmentFile.ts index 9018a540b..311fda21b 100644 --- a/Extension/src/Debugger/ParsedEnvironmentFile.ts +++ b/Extension/src/Debugger/ParsedEnvironmentFile.ts @@ -59,6 +59,17 @@ export class ParsedEnvironmentFile { value = value.replace(/(^['"]|['"]$)/g, ""); + // Resolve ${VAR} references against the variables defined so far in the file + // (and the process environment), following the dotenv interpolation rules. + // A reference to a name we don't know is left as-is instead of being dropped. + value = value.replace(/\$\{([\w\.\-]+)\}/g, (full: string, name: string): string => { + if (env.has(name)) { + return env.get(name); + } + const processValue: string | undefined = process.env[name]; + return processValue !== undefined ? processValue : full; + }); + env.set(key, value); } else { // Blank lines and lines starting with # are no parse errors diff --git a/Extension/test/scenarios/SingleRootProject/tests/ParsedEnvironmentFile.test.ts b/Extension/test/scenarios/SingleRootProject/tests/ParsedEnvironmentFile.test.ts index a107971ce..6e8abff01 100644 --- a/Extension/test/scenarios/SingleRootProject/tests/ParsedEnvironmentFile.test.ts +++ b/Extension/test/scenarios/SingleRootProject/tests/ParsedEnvironmentFile.test.ts @@ -100,4 +100,31 @@ MyName2=Value2 assertEnvironmentEqual(result.Env, "MyName1", "Value1"); assertEnvironmentEqual(result.Env, "MyName2", "Value2"); }); + + test("Resolve a reference to a variable defined earlier in the file", () => { + const content: string = `USER=admin +EMAIL=\${USER}@example.org`; + const result: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", []); + + assert(!result.Warning, `Failed to assert that Warning was empty: ${result.Warning}`); + assertEnvironmentEqual(result.Env, "USER", "admin"); + assertEnvironmentEqual(result.Env, "EMAIL", "admin@example.org"); + }); + + test("Resolve a reference from the initial environment", () => { + const initialEnv: Environment[] = [{ name: "USER", value: "admin" }]; + const content: string = `EMAIL=\${USER}@example.org`; + const result: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", initialEnv); + + assert(!result.Warning, `Failed to assert that Warning was empty: ${result.Warning}`); + assertEnvironmentEqual(result.Env, "EMAIL", "admin@example.org"); + }); + + test("Leave an unknown reference untouched", () => { + const content: string = `EMAIL=\${UNKNOWN_USER}@example.org`; + const result: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", []); + + assert(!result.Warning, `Failed to assert that Warning was empty: ${result.Warning}`); + assertEnvironmentEqual(result.Env, "EMAIL", "${UNKNOWN_USER}@example.org"); + }); });