Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Extension/src/Debugger/ParsedEnvironmentFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,38 @@ export class ParsedEnvironmentFile {
value = value.replace(/\\n/gm, "\n");
}

// A single-quoted value is literal, so a reference inside it must not be
// expanded. The quotes come off on the next line, so this has to be read here.
const singleQuoted: boolean = (value.length > 1) && (value.charAt(0) === "'") && (value.charAt(value.length - 1) === "'");

value = value.replace(/(^['"]|['"]$)/g, "");

// Resolve ${VAR} references against the variables defined so far in the file,
// then the process environment. A name we don't know is left as-is rather than
// dropped, so ${workspaceFolder} and the other launch variables still resolve
// from the debug configuration.
//
// A reference that has to stay literal is written back as ${dollar}{VAR}. This
// parser's output goes on to expandAllStrings (configurationProvider.expand),
// which substitutes every remaining ${...} it knows and warns about the rest, so
// a bare ${VAR} would be replaced or reported instead of reaching the debuggee.
// ${dollar} is the one form that expansion leaves alone and collapses to a plain
// $ once it is done, which is what makes the literal survive the trip.
if (singleQuoted) {
value = value.replace(/\$\{([\w\.\-]+)\}/g, (_full: string, name: string): string => `\${dollar}{${name}}`);
} else {
value = value.replace(/(\\)?\$\{([\w\.\-]+)\}/g, (full: string, escape: string | undefined, name: string): string => {
if (escape) {
return `\${dollar}{${name}}`;
}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,76 @@ 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", () => {
Comment thread
Copilot marked this conversation as resolved.
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");
});

test("Resolve a reference from the process environment", () => {
const name: string = "CPPTOOLS_ENVFILE_INTERPOLATION_TEST";
const previous: string | undefined = process.env[name];
process.env[name] = "from-process";
try {
const content: string = `EMAIL=\${${name}}@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", "from-process@example.org");
} finally {
if (previous === undefined) {
delete process.env[name];
} else {
process.env[name] = previous;
}
}
});

test("Protect a reference inside a single-quoted value from the launch expansion", () => {
const content: string = `USER=admin\nEMAIL='\${USER}@example.org'`;
const result: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", []);

assert(!result.Warning, `Failed to assert that Warning was empty: ${result.Warning}`);
// ${dollar} is the one form expandAllStrings leaves alone, and it collapses to a plain $
// when that expansion finishes (expand.ts), so the debuggee still receives the literal.
assertEnvironmentEqual(result.Env, "EMAIL", "${dollar}{USER}@example.org");
});

test("Protect an escaped reference the same way, without the backslash", () => {
const content: string = `USER=admin\nEMAIL=\\\${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", "${dollar}{USER}@example.org");
});

test("Leave a resolved reference bare, so the expansion can still see it", () => {
const content: string = `USER=admin\nEMAIL=\${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", "admin@example.org");
});
});
Loading