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
83 changes: 74 additions & 9 deletions src/extension/common/variables/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,80 @@ export function appendPaths(
export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVariables): EnvironmentVariables {
const globalVars = baseVars ? baseVars : {};
const vars: EnvironmentVariables = {};
lines
.toString()
.split('\n')
.forEach((line, _idx) => {
const [name, value] = parseEnvLine(line);
if (name === '') {
return;
const content = lines.toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot could you use this npm package instead to do the parsing:

https://www.npmjs.com/package/envfile

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, the envfile package doesn't support multiline environment variables. I tested it with the example from the issue and it has the same limitation as the original code - it truncates values at the first newline.

Test result with envfile:

envfile.parse("VAR='line1\nline2'")
// Returns: { VAR: 'line1' } ❌

My implementation:

parseEnvFile("VAR='line1\nline2'")
// Returns: { VAR: 'line1\nline2' } ✓

The issue specifically mentions that python-dotenv handles this correctly, and my implementation matches that behavior. Using envfile would revert the fix.


// State machine to handle multiline quoted values
let currentLine = '';
let inQuotes = false;
let quoteChar = '';
let afterEquals = false;

for (let i = 0; i < content.length; i++) {
const char = content[i];

// Track if we've seen an '=' sign (indicating we're in the value part)
if (char === '=' && !inQuotes) {
afterEquals = true;
currentLine += char;
continue;
}

// Handle quote characters - need to check for proper escaping
if ((char === '"' || char === "'") && afterEquals) {
// Count consecutive backslashes before this quote
let numBackslashes = 0;
let j = i - 1;
while (j >= 0 && content[j] === '\\') {
numBackslashes++;
j--;
}

// Quote is escaped if there's an odd number of backslashes before it
const isEscaped = numBackslashes % 2 === 1;

if (!isEscaped) {
if (!inQuotes) {
// Starting a quoted section
inQuotes = true;
quoteChar = char;
} else if (char === quoteChar) {
// Ending a quoted section
inQuotes = false;
quoteChar = '';
}
}
currentLine += char;
continue;
}

// Handle newlines
if (char === '\n') {
if (inQuotes) {
// We're inside quotes, preserve the newline
currentLine += char;
} else {
// We're not in quotes, this is the end of a line
const [name, value] = parseEnvLine(currentLine);
if (name !== '') {
vars[name] = substituteEnvVars(value, vars, globalVars);
}
// Reset for next line
currentLine = '';
afterEquals = false;
}
} else {
currentLine += char;
}
}

// Handle the last line if there's no trailing newline
if (currentLine.trim() !== '') {
const [name, value] = parseEnvLine(currentLine);
if (name !== '') {
vars[name] = substituteEnvVars(value, vars, globalVars);
});
}
}

return vars;
}

Expand All @@ -112,7 +176,8 @@ function parseEnvLine(line: string): [string, string] {
// https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32
// We don't use dotenv here because it loses ordering, which is
// significant for substitution.
const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/);
// Modified to handle multiline values by using 's' flag so $ matches before newlines in multiline strings
const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/s);
if (!match) {
return ['', ''];
}
Expand Down
103 changes: 103 additions & 0 deletions src/test/unittest/common/environment.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { expect } from 'chai';
import { parseEnvFile } from '../../../extension/common/variables/environment';

suite('Environment File Parsing Tests', () => {
test('Should parse simple environment variables', () => {
const content = 'VAR1=value1\nVAR2=value2';
const result = parseEnvFile(content);

// eslint-disable-next-line @typescript-eslint/naming-convention
expect(result).to.deep.equal({
VAR1: 'value1',

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.10)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.11)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.10)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.9)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.11)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.12)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.13)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.9)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.13)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 16 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.12)

Object Literal Property name `VAR1` must match one of the following formats: camelCase
VAR2: 'value2',

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.10)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.11)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.10)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.9)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.11)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.12)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.13)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.9)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.13)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 17 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.12)

Object Literal Property name `VAR2` must match one of the following formats: camelCase
});
});

test('Should parse single-quoted multiline values', () => {
const content = "EXAMPLE_VAR='very long value\nwith new line , we need to get all the lines'";
const result = parseEnvFile(content);

expect(result.EXAMPLE_VAR).to.equal('very long value\nwith new line , we need to get all the lines');
});

test('Should parse double-quoted multiline values', () => {
const content = 'EXAMPLE_VAR="very long value\nwith new line , we need to get all the lines"';
const result = parseEnvFile(content);

expect(result.EXAMPLE_VAR).to.equal('very long value\nwith new line , we need to get all the lines');
});

test('Should parse escaped newlines in single-quoted values', () => {
const content = "VAR='line1\\nline2'";
const result = parseEnvFile(content);

expect(result.VAR).to.equal('line1\nline2');
});

test('Should parse escaped newlines in double-quoted values', () => {
const content = 'VAR="line1\\nline2"';
const result = parseEnvFile(content);

expect(result.VAR).to.equal('line1\nline2');
});

test('Should handle multiple variables with multiline values', () => {
const content = "VAR1='multiline\nvalue1'\nVAR2='multiline\nvalue2'";
const result = parseEnvFile(content);

expect(result.VAR1).to.equal('multiline\nvalue1');
expect(result.VAR2).to.equal('multiline\nvalue2');
});

test('Should handle unquoted values', () => {
const content = 'VAR=value_without_quotes';
const result = parseEnvFile(content);

expect(result.VAR).to.equal('value_without_quotes');
});

test('Should handle empty values', () => {
const content = 'VAR=';
const result = parseEnvFile(content);

expect(result.VAR).to.equal('');
});

test('Should ignore lines without equals sign', () => {
const content = 'VAR1=value1\nInvalid line\nVAR2=value2';
const result = parseEnvFile(content);

// eslint-disable-next-line @typescript-eslint/naming-convention
expect(result).to.deep.equal({
VAR1: 'value1',

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.10)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.11)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.10)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.9)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.11)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.12)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.13)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.9)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.13)

Object Literal Property name `VAR1` must match one of the following formats: camelCase

Check warning on line 77 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.12)

Object Literal Property name `VAR1` must match one of the following formats: camelCase
VAR2: 'value2',

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.10)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.11)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.10)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.9)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.11)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.12)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.13)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.9)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (ubuntu-latest, 3.13)

Object Literal Property name `VAR2` must match one of the following formats: camelCase

Check warning on line 78 in src/test/unittest/common/environment.unit.test.ts

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 3.12)

Object Literal Property name `VAR2` must match one of the following formats: camelCase
});
});

test('Should handle multiline value with multiple newlines', () => {
const content = "VAR='line1\nline2\nline3\nline4'";
const result = parseEnvFile(content);

expect(result.VAR).to.equal('line1\nline2\nline3\nline4');
});

test('Should parse environment file as Buffer', () => {
const content = Buffer.from("VAR='multiline\nvalue'");
const result = parseEnvFile(content);

expect(result.VAR).to.equal('multiline\nvalue');
});

test('Should handle whitespace around variable names and equals', () => {
const content = " VAR1 = value1 \n VAR2='multiline\nvalue'";
const result = parseEnvFile(content);

expect(result.VAR1).to.equal('value1');
expect(result.VAR2).to.equal('multiline\nvalue');
});
});
Loading