Skip to content
Merged
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
5 changes: 5 additions & 0 deletions Extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\".",
Expand Down
85 changes: 85 additions & 0 deletions Extension/src/Debugger/ParsedEnvironmentFile.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> = 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);
}
}
32 changes: 31 additions & 1 deletion Extension/src/Debugger/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -188,14 +189,32 @@ 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);
} else {
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
Expand Down Expand Up @@ -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<void> {
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 {
Expand Down
112 changes: 112 additions & 0 deletions Extension/test/unitTests/ParsedEnvironmentFile.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
5 changes: 5 additions & 0 deletions Extension/tools/OptionsSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\".",
Expand Down
20 changes: 20 additions & 0 deletions launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```