Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-pass environment variable resolution #2322

Merged
merged 11 commits into from
Jul 24, 2018
2 changes: 1 addition & 1 deletion Extension/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test"
"--extensionTestsPath=${workspaceFolder}/out/test/unitTests"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, that's interesting...I guess we don't Launch Tests very much :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks good so far. I'm not done reviewing yet...

],
"stopOnEntry": false,
"sourceMaps": true,
Expand Down
157 changes: 113 additions & 44 deletions Extension/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,107 @@ export function showReleaseNotes(): void {
vscode.commands.executeCommand('vscode.previewHtml', vscode.Uri.file(getExtensionFilePath("ReleaseNotes.html")), vscode.ViewColumn.One, "C/C++ Extension Release Notes");
}

function indexOfMatchingCloseBrace(startingPos: number, input: string): number {
let closeBracesToFind: number = 1;
for (let i: number = startingPos; i < input.length; i++) {
if (input[i] === "$" && i + 1 < input.length && input[i + 1] === "{") {
closeBracesToFind += 1;
} else if (input[i] === "}") {
closeBracesToFind -= 1;
if (closeBracesToFind < 1) {
return i;
}
}
}
return -1;
}

interface StackEntry {
prefix: string;
postfix: string;
}

function expandNestedVariable(input: string, additionalEnvironment: {[key: string]: string | string[]}): string {
let stack: StackEntry[] = [];
let stackInput: string = input;
while (stackInput !== "") {
const openIndex: number = stackInput.indexOf("${");
if (openIndex < 0) {
break;
}

const closeIndex: number = indexOfMatchingCloseBrace(openIndex + 2, stackInput);
if (closeIndex < 0) {
break;
}

stack.push({
prefix: stackInput.substring(0, openIndex),
postfix: stackInput.substring(closeIndex + 1)
});
stackInput = stackInput.substring(openIndex + 2, closeIndex);
}

let ret: string = stackInput;
while (stack.length !== 0) {
let stackEntry: StackEntry = stack.pop();
let resolvedItem: string = resolveSingleVariableBlock(input, "${" + ret + "}", ret, additionalEnvironment);
ret = stackEntry.prefix + resolvedItem + stackEntry.postfix;
}

return ret;
}

function resolveSingleVariableBlock(originalInput: string, wholeVariable: string, innerVariable: string, additionalEnvironment: {[key: string]: string | string[]}): string {
let varType: string;
let name: string = innerVariable.replace(/(env|config|workspaceFolder)[\.|:](.*)/, (ignored: string, type: string, rest: string) => {
varType = type;
return rest;
});

// Historically, if the variable didn't have anything before the "." or ":"
// it was assumed to be an environment variable
if (varType === undefined) {
varType = "env";
}
let newValue: string = undefined;
switch (varType) {
case "env": {
let v: string | string[] = additionalEnvironment[name];
if (typeof v === "string") {
newValue = v;
} else if (originalInput === wholeVariable && v instanceof Array) {
newValue = v.join(";");
}
if (!newValue) {
newValue = process.env[name];
}
break;
}
case "config": {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration();
if (config) {
newValue = config.get<string>(name);
}
break;
}
case "workspaceFolder": {
// Only replace ${workspaceFolder:name} variables for now.
// We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also
// intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions)
if (name && vscode.workspace && vscode.workspace.workspaceFolders) {
let folder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase());
if (folder) {
newValue = folder.uri.fsPath;
}
}
break;
}
default: { assert.fail("unknown varType matched"); }
}
return (newValue) ? newValue : wholeVariable;
}

export function resolveVariables(input: string, additionalEnvironment: {[key: string]: string | string[]}): string {
if (!input) {
return "";
Expand All @@ -178,53 +279,21 @@ export function resolveVariables(input: string, additionalEnvironment: {[key: st
}

// Replace environment and configuration variables.
let regexp: RegExp = /\$\{((env|config|workspaceFolder)(.|:))?(.*?)\}/g;
let ret: string = input.replace(regexp, (match: string, ignored1: string, varType: string, ignored2: string, name: string) => {
// Historically, if the variable didn't have anything before the "." or ":"
// it was assumed to be an environment variable
if (varType === undefined) {
varType = "env";
let ret: string = input;
const cycleCache: Set<string> = new Set([ ret ]);
const openTagRegex: RegExp = /\$\{/;
while (openTagRegex.test(ret)) {
const expansion: string = expandNestedVariable(ret, additionalEnvironment);
const doneExpanding: boolean = cycleCache.has(expansion);
if (doneExpanding) {
break;
}
let newValue: string = undefined;
switch (varType) {
case "env": {
let v: string | string[] = additionalEnvironment[name];
if (typeof v === "string") {
newValue = v;
} else if (input === match && v instanceof Array) {
newValue = v.join(";");
}
if (!newValue) {
newValue = process.env[name];
}
break;
}
case "config": {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration();
if (config) {
newValue = config.get<string>(name);
}
break;
}
case "workspaceFolder": {
// Only replace ${workspaceFolder:name} variables for now.
// We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also
// intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions)
if (name && vscode.workspace && vscode.workspace.workspaceFolders) {
let folder: vscode.WorkspaceFolder = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase());
if (folder) {
newValue = folder.uri.fsPath;
}
}
break;
}
default: { assert.fail("unknown varType matched"); }
}
return (newValue) ? newValue : match;
});
cycleCache.add(expansion);
ret = expansion;
}

// Resolve '~' at the start of the path.
regexp = /^\~/g;
let regexp: RegExp = /^\~/g;
ret = ret.replace(regexp, (match: string, name: string) => {
let newValue: string = process.env.HOME;
return (newValue) ? newValue : match;
Expand Down
Loading