From a4fb81d05da4a66a963c903a54c337dc5dcf95ef Mon Sep 17 00:00:00 2001 From: Mara Jochum Date: Sun, 1 May 2022 15:57:39 +0200 Subject: [PATCH 1/3] Resolve env vars in settings.json --- .../configuration/common/configuration.ts | 132 +++++++++++++++++- .../test/common/configuration.test.ts | 24 +++- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 6ed2b0c3ef8f7..b86a6d313dfd5 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -8,6 +8,15 @@ import * as types from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { localize } from 'vs/nls'; + +type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined }; + +const VARIABLE_LHS = '${'; +const VARIABLE_REGEXP = /\$\{(.*?)\}.*$/g; +const VARIABLE_REGEXP_ENV = /\$\{env:(.*?)\}.*$/g; export const IConfigurationService = createDecorator('configurationService'); @@ -208,7 +217,12 @@ export function addToValueTree(settingsTreeRoot: any, key: string, value: any, c if (typeof curr === 'object' && curr !== null) { try { - curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606 + if (JSON.stringify(value).match(VARIABLE_REGEXP_ENV)) { + const result = resolveWithEnvironment({ ...process.env, userHome: undefined }, undefined, value); + curr[last] = result; + } else { + curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606 + } } catch (e) { conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`); } @@ -257,7 +271,8 @@ export function getConfigurationValue(config: any, settingPath: string, defau } const path = settingPath.split('.'); - const result = accessSetting(config, path); + let result = accessSetting(config, path); + //result = resolveWithEnvironment({ ...process.env }, undefined, result); return typeof result === 'undefined' ? defaultValue : result; } @@ -294,3 +309,116 @@ export function getMigratedSettingValue(configurationService: IConfigurationS export function getLanguageTagSettingPlainKey(settingKey: string) { return settingKey.replace(/[\[\]]/g, ''); } + +function prepareEnv(envVariables: IProcessEnvironment): IProcessEnvironment { + // windows env variables are case insensitive + if (isWindows) { + const ev: IProcessEnvironment = Object.create(null); + Object.keys(envVariables).forEach(key => { + ev[key.toLowerCase()] = envVariables[key]; + }); + return ev; + } + return envVariables; +} + +function resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): any { + return recursiveResolve({ env: prepareEnv(environment), userHome: undefined }, root ? root.uri : undefined, value); +} + +function recursiveResolve(environment: Environment, folderUri: URI | undefined, value: any, commandValueMapping?: IStringDictionary, resolvedVariables?: Map): any { + if (types.isString(value)) { + return resolveString(environment, folderUri, value, commandValueMapping, resolvedVariables); + } else if (types.isArray(value)) { + return value.map(s => recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables)); + } else if (types.isObject(value)) { + let result: IStringDictionary | string[]> = Object.create(null); + let replaced = Object.entries(value).map(([k, v]) => { + if (types.isString(v) && v.match(VARIABLE_REGEXP_ENV)) { + return [k, resolveString(environment, folderUri, v, commandValueMapping, resolvedVariables)]; + } else { + return [k, v]; + } + }); + // two step process to preserve object key order + for (const [key, value] of replaced) { + result[key] = value; + } + return result; + } + return value; +} + +function resolveString(environment: Environment, folderUri: URI | undefined, value: string, commandValueMapping: IStringDictionary | undefined, resolvedVariables?: Map): any { + // loop through all variables occurrences in 'value' + return replaceSync(value, VARIABLE_REGEXP, (match: string, variable: string) => { + // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. + if (variable.includes(VARIABLE_LHS)) { + return match; + } + + let resolvedValue = evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); + + if (resolvedVariables) { + resolvedVariables.set(variable, resolvedValue); + } + + if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(VARIABLE_REGEXP)) { + resolvedValue = resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); + } + + resolvedValue = match.replace(`\$\{${variable}\}`, resolvedValue); + return resolvedValue; + }); +} + +function evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: URI | undefined, commandValueMapping: IStringDictionary | undefined) { + + // try to separate variable arguments from variable name + let argument: string | undefined; + const parts = variable.split(':'); + if (parts.length > 1) { + variable = parts[0]; + argument = parts[1]; + } + + switch (variable) { + + case 'env': + if (argument) { + if (environment.env) { + // Depending on the source of the environment, on Windows, the values may all be lowercase. + const env = environment.env[isWindows ? argument.toLowerCase() : argument]; + if (types.isString(env)) { + return env; + } + } + // For `env` we should do the same as a normal shell does - evaluates undefined envs to an empty string #46436 + return ''; + } + throw new Error(localize('missingEnvVarName', "Variable {0} can not be resolved because no environment variable name is given.", match)); + + default: { + return ''; + } + } +} + +function replaceSync(str: string, search: RegExp, replacer: (match: string, ...args: any[]) => string): string { + let parts: (string)[] = []; + + let last = 0; + for (const match of str.matchAll(search)) { + parts.push(str.slice(last, match.index)); + if (match.index === undefined) { + throw new Error('match.index should be defined'); + } + + last = match.index + match[0].length; + parts.push(replacer(match[0], ...match.slice(1), match.index, str, match.groups)); + } + + parts.push(str.slice(last)); + + return parts.join(''); +} diff --git a/src/vs/platform/configuration/test/common/configuration.test.ts b/src/vs/platform/configuration/test/common/configuration.test.ts index f76e9d20518b2..13ceb8fc017d7 100644 --- a/src/vs/platform/configuration/test/common/configuration.test.ts +++ b/src/vs/platform/configuration/test/common/configuration.test.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { merge, removeFromValueTree } from 'vs/platform/configuration/common/configuration'; +import { merge, addToValueTree, removeFromValueTree } from 'vs/platform/configuration/common/configuration'; import { mergeChanges } from 'vs/platform/configuration/common/configurationModels'; suite('Configuration', () => { + const OLD_ENV = process.env; + process.env = { ...OLD_ENV }; + test('simple merge', () => { let base = { 'a': 1, 'b': 2 }; merge(base, { 'a': 3, 'c': 4 }, true); @@ -117,6 +120,25 @@ suite('Configuration', () => { assert.deepStrictEqual(target, { 'a': { 'b': { 'd': 1 } } }); }); + test('addToValueTree: add key with value ${env:...}', () => { + process.env['VALUE_FOR_B'] = '2'; + + let target = { 'a': 1 }; + addToValueTree(target, 'b', '${env:VALUE_FOR_B}', e => { throw new Error(e); }); + assert.deepStrictEqual(target, { 'a': 1, 'b': '2' }); + + process.env = OLD_ENV; + }); + + test('addToValueTree: add key with value string containing ${env:...}', () => { + process.env['VALUE_FOR_B'] = '2'; + + let target = { 'a': 'string1' }; + addToValueTree(target, 'b', 'string${env:VALUE_FOR_B}', e => { throw new Error(e); }); + assert.deepStrictEqual(target, { 'a': 'string1', 'b': 'string2' }); + + process.env = OLD_ENV; + }); }); suite('Configuration Changes: Merge', () => { From 7c16aaaad6ef3de183aa640397a2c2a0ea22468c Mon Sep 17 00:00:00 2001 From: Mara Jochum Date: Sun, 1 May 2022 20:32:39 +0200 Subject: [PATCH 2/3] Cleaned up types --- .../platform/configuration/common/configuration.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index b86a6d313dfd5..905e27c0a5611 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -333,12 +333,9 @@ function recursiveResolve(environment: Environment, folderUri: URI | undefined, return value.map(s => recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables)); } else if (types.isObject(value)) { let result: IStringDictionary | string[]> = Object.create(null); - let replaced = Object.entries(value).map(([k, v]) => { - if (types.isString(v) && v.match(VARIABLE_REGEXP_ENV)) { - return [k, resolveString(environment, folderUri, v, commandValueMapping, resolvedVariables)]; - } else { - return [k, v]; - } + const replaced = Object.keys(value).map(key => { + const replaced = resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); + return [replaced, recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables)] as const; }); // two step process to preserve object key order for (const [key, value] of replaced) { @@ -349,7 +346,7 @@ function recursiveResolve(environment: Environment, folderUri: URI | undefined, return value; } -function resolveString(environment: Environment, folderUri: URI | undefined, value: string, commandValueMapping: IStringDictionary | undefined, resolvedVariables?: Map): any { +function resolveString(environment: Environment, folderUri: URI | undefined, value: string, commandValueMapping: IStringDictionary | undefined, resolvedVariables?: Map): string { // loop through all variables occurrences in 'value' return replaceSync(value, VARIABLE_REGEXP, (match: string, variable: string) => { // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. @@ -372,7 +369,7 @@ function resolveString(environment: Environment, folderUri: URI | undefined, val }); } -function evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: URI | undefined, commandValueMapping: IStringDictionary | undefined) { +function evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: URI | undefined, commandValueMapping: IStringDictionary | undefined): string { // try to separate variable arguments from variable name let argument: string | undefined; From c107058b58f7608753f32820fbe47bc492f5f415 Mon Sep 17 00:00:00 2001 From: Mara Jochum Date: Sun, 1 May 2022 22:57:49 +0200 Subject: [PATCH 3/3] Clean up --- src/vs/platform/configuration/common/configuration.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 905e27c0a5611..09b0df8185ade 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -271,8 +271,7 @@ export function getConfigurationValue(config: any, settingPath: string, defau } const path = settingPath.split('.'); - let result = accessSetting(config, path); - //result = resolveWithEnvironment({ ...process.env }, undefined, result); + const result = accessSetting(config, path); return typeof result === 'undefined' ? defaultValue : result; }