diff --git a/Extension/package.json b/Extension/package.json index cc3c44d1a..8ccaa5c21 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -1201,6 +1201,11 @@ }, "default": [] }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions. These file has key value pairs sepearted by an equals sign per line. E.g. KEY=VALUE", + "default": "${workspaceFolder}/.env" + }, "symbolSearchPath": { "type": "string", "description": "Semicolon separated list of directories to use to search for symbol (that is, pdb) files. Example: \"c:\\dir1;c:\\dir2\".", diff --git a/Extension/src/Debugger/ParsedEnvironmentFile.ts b/Extension/src/Debugger/ParsedEnvironmentFile.ts new file mode 100644 index 000000000..151ca7416 --- /dev/null +++ b/Extension/src/Debugger/ParsedEnvironmentFile.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; + +export interface Environment { + name: string; + value: string; +} + +export class ParsedEnvironmentFile { + public Env: Environment[]; + public Warning: string | null; + + private constructor(env: Environment[], warning: string | null) { + this.Env = env; + this.Warning = warning; + } + + public static CreateFromFile(envFile: string, initialEnv: Environment[] | undefined): ParsedEnvironmentFile { + let content: string = fs.readFileSync(envFile, "utf8"); + return this.CreateFromContent(content, envFile, initialEnv); + } + + public static CreateFromContent(content: string, envFile: string, initialEnv: Environment[] | undefined): ParsedEnvironmentFile { + + // Remove UTF-8 BOM if present + if (content.charAt(0) === '\uFEFF') { + content = content.substr(1); + } + + let parseErrors: string[] = []; + let env: Map = new Map(); + + if (initialEnv) { + // Convert array to map to prevent duplicate keys being created. + // If a duplicate key is found, replace it. + initialEnv.forEach((e) => { + env.set(e.name, e.value); + }); + } + + content.split("\n").forEach(line => { + // Split the line between key and value + const r: RegExpMatchArray = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); + + if (r !== null) { + const key: string = r[1]; + let value: string = r[2] || ""; + if ((value.length > 0) && (value.charAt(0) === '"') && (value.charAt(value.length - 1) === '"')) { + value = value.replace(/\\n/gm, "\n"); + } + + value = value.replace(/(^['"]|['"]$)/g, ""); + + env.set(key, value); + } else { + // Blank lines and lines starting with # are no parse errors + const comments: RegExp = new RegExp(/^\s*(#|$)/); + if (!comments.test(line)) { + parseErrors.push(line); + } + } + }); + + // show error message if single lines cannot get parsed + let warning: string = null; + if (parseErrors.length !== 0) { + warning = "Ignoring non-parseable lines in envFile " + envFile + ": "; + parseErrors.forEach(function (value, idx, array): void { + warning += "\"" + value + "\"" + ((idx !== array.length - 1) ? ", " : "."); + }); + } + + // Convert env map back to array. + const arrayEnv: Environment[] = []; + for (let key of env.keys()) { + arrayEnv.push({name: key, value: env.get(key)}); + } + + return new ParsedEnvironmentFile(arrayEnv, warning); + } +} \ No newline at end of file diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 7c312b3b6..385bab4ce 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -16,6 +16,7 @@ import { buildAndDebugActiveFileStr } from './extension'; import { IConfiguration, IConfigurationSnippet, DebuggerType, MIConfigurations, WindowsConfigurations, WSLConfigurations, PipeTransportConfigurations } from './configurations'; import { parse } from 'jsonc-parser'; import { PlatformInformation } from '../platform'; +import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; function isDebugLaunchStr(str: string): boolean { return str === "(gdb) Launch" || str === "(lldb) Launch" || str === "(Windows) Launch"; @@ -188,7 +189,7 @@ class CppConfigurationProvider implements vscode.DebugConfigurationProvider { // Disable debug heap by default, enable if 'enableDebugHeap' is set. if (!config.enableDebugHeap) { - const disableDebugHeapEnvSetting : any = {"name" : "_NO_DEBUG_HEAP", "value" : "1"}; + const disableDebugHeapEnvSetting : Environment = {"name" : "_NO_DEBUG_HEAP", "value" : "1"}; if (config.environment && util.isArray(config.environment)) { config.environment.push(disableDebugHeapEnvSetting); @@ -196,6 +197,24 @@ class CppConfigurationProvider implements vscode.DebugConfigurationProvider { config.environment = [disableDebugHeapEnvSetting]; } } + + // Add environment variables from .env file + if (config.envFile) { + try { + const parsedFile: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromFile(config.envFile.replace(/\${workspaceFolder}/g, folder.uri.path), config["environment"]); + + // show error message if single lines cannot get parsed + if (parsedFile.Warning) { + CppConfigurationProvider.showFileWarningAsync(parsedFile.Warning, config.envFile); + } + + config.environment = parsedFile.Env; + + delete config.envFile; + } catch (e) { + throw new Error("Can't parse envFile " + config.envFile); + } + } } // Modify WSL config for OpenDebugAD7 @@ -224,6 +243,17 @@ class CppConfigurationProvider implements vscode.DebugConfigurationProvider { // if config or type is not specified, return null to trigger VS Code to open a configuration file https://github.com/Microsoft/vscode/issues/54213 return config && config.type ? config : null; } + + private static async showFileWarningAsync(message: string, fileName: string) : Promise { + const openItem: vscode.MessageItem = { title: 'Open envFile' }; + let result: vscode.MessageItem = await vscode.window.showWarningMessage(message, openItem); + if (result && result.title === openItem.title) { + let doc: vscode.TextDocument = await vscode.workspace.openTextDocument(fileName); + if (doc) { + vscode.window.showTextDocument(doc); + } + } + } } export class CppVsDbgConfigurationProvider extends CppConfigurationProvider { diff --git a/Extension/test/unitTests/ParsedEnvironmentFile.test.ts b/Extension/test/unitTests/ParsedEnvironmentFile.test.ts new file mode 100644 index 000000000..0afec524d --- /dev/null +++ b/Extension/test/unitTests/ParsedEnvironmentFile.test.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Environment, ParsedEnvironmentFile } from '../../src/Debugger/ParsedEnvironmentFile' +import * as assert from 'assert'; + +// Because the environment variable is set as an array, the index does not matter. +function AssertEnvironmentEqual(env: Environment[], name: string, value: string) { + let found: boolean = false; + for (let e of env) + { + if (e.name == name) + { + assert(e.value == value, `Checking if ${e.value} == ${value}`); + found = true; + break; + } + } + assert(found, `${name} was not found in env.`) +} + +suite("ParsedEnvironmentFile", () => { + test("Add single variable", () => { + const content = `MyName=VALUE`; + const fakeConfig : Environment[] = []; + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]); + + assert(result.Warning == null, `Failed to assert that Warning was empty: ${result.Warning}`); + AssertEnvironmentEqual(result.Env, "MyName", "VALUE"); + }); + + test("Handle quoted values", () => { + const content = `MyName="VALUE"`; + const fakeConfig : Environment[] = []; + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]); + + assert(result.Warning == null, `Failed to assert that Warning was empty: ${result.Warning}`); + AssertEnvironmentEqual(result.Env, "MyName", "VALUE"); + }); + + test("Handle BOM", () => { + const content = "\uFEFFMyName=VALUE"; + const fakeConfig : Environment[] = []; + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]); + + assert(result.Warning == null, `Failed to assert that Warning was empty: ${result.Warning}`); + AssertEnvironmentEqual(result.Env, "MyName", "VALUE"); + }); + + test("Add multiple variables", () => { + const content = ` +MyName1=Value1 +MyName2=Value2 + +`; + const fakeConfig : Environment[] = []; + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]); + + assert(result.Warning == null, `Failed to assert that Warning was empty: ${result.Warning}`); + AssertEnvironmentEqual(result.Env, "MyName1", "Value1"); + AssertEnvironmentEqual(result.Env, "MyName2", "Value2"); + }); + + test("Update variable", () => { + const content = ` +MyName1=Value1 +MyName2=Value2 + +`; + const initialEnv : Environment[] = []; + initialEnv.push({name : "MyName1", value: "Value7"}); + initialEnv.push({name : "ThisShouldNotChange", value : "StillHere"}); + + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", initialEnv); + + assert(result.Warning == null, `Failed to assert that Warning was empty: ${result.Warning}`); + AssertEnvironmentEqual(result.Env, "MyName1", "Value1"); + AssertEnvironmentEqual(result.Env, "ThisShouldNotChange", "StillHere"); + AssertEnvironmentEqual(result.Env, "MyName2", "Value2"); + }); + + test("Handle comments", () => { + const content = `# This is an environment file +MyName1=Value1 +# This is a comment in the middle of the file +MyName2=Value2 +`; + const fakeConfig : Environment[] = []; + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]); + + assert(result.Warning == null, `Failed to assert that Warning was empty: ${result.Warning}`); + AssertEnvironmentEqual(result.Env, "MyName1", "Value1"); + AssertEnvironmentEqual(result.Env, "MyName2", "Value2"); + }); + + test("Handle invalid lines", () => { + const content = ` +This_Line_Is_Wrong +MyName1=Value1 +MyName2=Value2 + +`; + const fakeConfig : Environment[] = []; + const result = ParsedEnvironmentFile.CreateFromContent(content, "TestEnvFileName", fakeConfig["env"]); + + assert(result.Warning.startsWith("Ignoring non-parseable lines in envFile TestEnvFileName"), 'Checking if warning exists'); + AssertEnvironmentEqual(result.Env, "MyName1", "Value1"); + AssertEnvironmentEqual(result.Env, "MyName2", "Value2"); + }); +}); \ No newline at end of file diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 002642495..56a144a04 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -429,6 +429,11 @@ }, "default": [] }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions. These file has key value pairs sepearted by an equals sign per line. E.g. KEY=VALUE", + "default": "${workspaceFolder}/.env" + }, "symbolSearchPath": { "type": "string", "description": "Semicolon separated list of directories to use to search for symbol (that is, pdb) files. Example: \"c:\\dir1;c:\\dir2\".", diff --git a/launch.md b/launch.md index bbdf08e07..2c6f4d974 100644 --- a/launch.md +++ b/launch.md @@ -194,3 +194,23 @@ _Note: core dump debugging is not supported with MinGw._ * #### `sourceFileMap` This allows mapping of the compile time paths for source to local source locations. It is an object of key/value pairs and will resolve the first string-matched path. (example: `"sourceFileMap": { "/mnt/c": "c:\\" }` will map any path returned by the debugger that begins with `/mnt/c` and convert it to `c:\\`. You can have multiple mappings in the object but they will be handled in the order provided.) + +## Environment variable definitions file + +An environment variable definitions file is a simple text file containing key-value pairs in the form of `environment_variable=value`, with `#` used for comments. Multiline values are not supported. + +The `cppvsdbg` debugger configuration also contains an `envFile` property that allows you to easily set variables for debugging purposes. + +For example: + +**project.env file** + +```bash +# project.env + +# Example environment with key as 'MYENVRIONMENTPATH' and value as C:\\Users\\USERNAME\\Project +MYENVRIONMENTPATH=C:\\Users\\USERNAME\\Project + +# Variables with spaces +SPACED_OUT_PATH="C:\\This Has Spaces\\Project" +``` \ No newline at end of file