Skip to content

Commit 45a8ecc

Browse files
Copilotrchiodo
andcommitted
Fix multiline environment variable parsing in .env files
Co-authored-by: rchiodo <[email protected]>
1 parent 4b2a2d0 commit 45a8ecc

File tree

2 files changed

+165
-9
lines changed

2 files changed

+165
-9
lines changed

src/extension/common/variables/environment.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,68 @@ export function appendPaths(
9494
export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVariables): EnvironmentVariables {
9595
const globalVars = baseVars ? baseVars : {};
9696
const vars: EnvironmentVariables = {};
97-
lines
98-
.toString()
99-
.split('\n')
100-
.forEach((line, _idx) => {
101-
const [name, value] = parseEnvLine(line);
102-
if (name === '') {
103-
return;
97+
const content = lines.toString();
98+
99+
// State machine to handle multiline quoted values
100+
let currentLine = '';
101+
let inQuotes = false;
102+
let quoteChar = '';
103+
let afterEquals = false;
104+
105+
for (let i = 0; i < content.length; i++) {
106+
const char = content[i];
107+
const prevChar = i > 0 ? content[i - 1] : '';
108+
109+
// Track if we've seen an '=' sign (indicating we're in the value part)
110+
if (char === '=' && !inQuotes) {
111+
afterEquals = true;
112+
currentLine += char;
113+
continue;
114+
}
115+
116+
// Handle quote characters
117+
if ((char === '"' || char === "'") && afterEquals && prevChar !== '\\') {
118+
if (!inQuotes) {
119+
// Starting a quoted section
120+
inQuotes = true;
121+
quoteChar = char;
122+
} else if (char === quoteChar) {
123+
// Ending a quoted section
124+
inQuotes = false;
125+
quoteChar = '';
126+
}
127+
currentLine += char;
128+
continue;
129+
}
130+
131+
// Handle newlines
132+
if (char === '\n') {
133+
if (inQuotes) {
134+
// We're inside quotes, preserve the newline
135+
currentLine += char;
136+
} else {
137+
// We're not in quotes, this is the end of a line
138+
const [name, value] = parseEnvLine(currentLine);
139+
if (name !== '') {
140+
vars[name] = substituteEnvVars(value, vars, globalVars);
141+
}
142+
// Reset for next line
143+
currentLine = '';
144+
afterEquals = false;
104145
}
146+
} else {
147+
currentLine += char;
148+
}
149+
}
150+
151+
// Handle the last line if there's no trailing newline
152+
if (currentLine.trim() !== '') {
153+
const [name, value] = parseEnvLine(currentLine);
154+
if (name !== '') {
105155
vars[name] = substituteEnvVars(value, vars, globalVars);
106-
});
156+
}
157+
}
158+
107159
return vars;
108160
}
109161

@@ -112,7 +164,8 @@ function parseEnvLine(line: string): [string, string] {
112164
// https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32
113165
// We don't use dotenv here because it loses ordering, which is
114166
// significant for substitution.
115-
const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/);
167+
// Modified to handle multiline values by using 's' flag to make . match newlines
168+
const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/s);
116169
if (!match) {
117170
return ['', ''];
118171
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { expect } from 'chai';
7+
import { parseEnvFile } from '../../../extension/common/variables/environment';
8+
9+
suite('Environment File Parsing Tests', () => {
10+
test('Should parse simple environment variables', () => {
11+
const content = 'VAR1=value1\nVAR2=value2';
12+
const result = parseEnvFile(content);
13+
14+
// eslint-disable-next-line @typescript-eslint/naming-convention
15+
expect(result).to.deep.equal({
16+
VAR1: 'value1',
17+
VAR2: 'value2',
18+
});
19+
});
20+
21+
test('Should parse single-quoted multiline values', () => {
22+
const content = "EXAMPLE_VAR='very long value\nwith new line , we need to get all the lines'";
23+
const result = parseEnvFile(content);
24+
25+
expect(result.EXAMPLE_VAR).to.equal('very long value\nwith new line , we need to get all the lines');
26+
});
27+
28+
test('Should parse double-quoted multiline values', () => {
29+
const content = 'EXAMPLE_VAR="very long value\nwith new line , we need to get all the lines"';
30+
const result = parseEnvFile(content);
31+
32+
expect(result.EXAMPLE_VAR).to.equal('very long value\nwith new line , we need to get all the lines');
33+
});
34+
35+
test('Should parse escaped newlines in single-quoted values', () => {
36+
const content = "VAR='line1\\nline2'";
37+
const result = parseEnvFile(content);
38+
39+
expect(result.VAR).to.equal('line1\nline2');
40+
});
41+
42+
test('Should parse escaped newlines in double-quoted values', () => {
43+
const content = 'VAR="line1\\nline2"';
44+
const result = parseEnvFile(content);
45+
46+
expect(result.VAR).to.equal('line1\nline2');
47+
});
48+
49+
test('Should handle multiple variables with multiline values', () => {
50+
const content = "VAR1='multiline\nvalue1'\nVAR2='multiline\nvalue2'";
51+
const result = parseEnvFile(content);
52+
53+
expect(result.VAR1).to.equal('multiline\nvalue1');
54+
expect(result.VAR2).to.equal('multiline\nvalue2');
55+
});
56+
57+
test('Should handle unquoted values', () => {
58+
const content = 'VAR=value_without_quotes';
59+
const result = parseEnvFile(content);
60+
61+
expect(result.VAR).to.equal('value_without_quotes');
62+
});
63+
64+
test('Should handle empty values', () => {
65+
const content = 'VAR=';
66+
const result = parseEnvFile(content);
67+
68+
expect(result.VAR).to.equal('');
69+
});
70+
71+
test('Should ignore lines without equals sign', () => {
72+
const content = 'VAR1=value1\nInvalid line\nVAR2=value2';
73+
const result = parseEnvFile(content);
74+
75+
// eslint-disable-next-line @typescript-eslint/naming-convention
76+
expect(result).to.deep.equal({
77+
VAR1: 'value1',
78+
VAR2: 'value2',
79+
});
80+
});
81+
82+
test('Should handle multiline value with multiple newlines', () => {
83+
const content = "VAR='line1\nline2\nline3\nline4'";
84+
const result = parseEnvFile(content);
85+
86+
expect(result.VAR).to.equal('line1\nline2\nline3\nline4');
87+
});
88+
89+
test('Should parse environment file as Buffer', () => {
90+
const content = Buffer.from("VAR='multiline\nvalue'");
91+
const result = parseEnvFile(content);
92+
93+
expect(result.VAR).to.equal('multiline\nvalue');
94+
});
95+
96+
test('Should handle whitespace around variable names and equals', () => {
97+
const content = " VAR1 = value1 \n VAR2='multiline\nvalue'";
98+
const result = parseEnvFile(content);
99+
100+
expect(result.VAR1).to.equal('value1');
101+
expect(result.VAR2).to.equal('multiline\nvalue');
102+
});
103+
});

0 commit comments

Comments
 (0)