From 4b2a2d0ccc329cda0cecf4478d7c748eeb32036a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:07:54 +0000 Subject: [PATCH 1/5] Initial plan From 45a8ecc66811bfb7c4830158f9ff045a56e2be77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:16:17 +0000 Subject: [PATCH 2/5] Fix multiline environment variable parsing in .env files Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- src/extension/common/variables/environment.ts | 71 ++++++++++-- .../unittest/common/environment.unit.test.ts | 103 ++++++++++++++++++ 2 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 src/test/unittest/common/environment.unit.test.ts diff --git a/src/extension/common/variables/environment.ts b/src/extension/common/variables/environment.ts index 0fd45e1b..4622a4c9 100644 --- a/src/extension/common/variables/environment.ts +++ b/src/extension/common/variables/environment.ts @@ -94,16 +94,68 @@ 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(); + + // 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]; + const prevChar = i > 0 ? content[i - 1] : ''; + + // 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 + if ((char === '"' || char === "'") && afterEquals && prevChar !== '\\') { + 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; } @@ -112,7 +164,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 to make . match newlines + const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/s); if (!match) { return ['', '']; } diff --git a/src/test/unittest/common/environment.unit.test.ts b/src/test/unittest/common/environment.unit.test.ts new file mode 100644 index 00000000..a3a9e983 --- /dev/null +++ b/src/test/unittest/common/environment.unit.test.ts @@ -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', + VAR2: 'value2', + }); + }); + + 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', + VAR2: 'value2', + }); + }); + + 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'); + }); +}); From b673e6103573e015362a9d73526fa1bb02b65512 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:19:56 +0000 Subject: [PATCH 3/5] Improve escape sequence handling in environment parser Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- src/extension/common/variables/environment.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/extension/common/variables/environment.ts b/src/extension/common/variables/environment.ts index 4622a4c9..30b0157f 100644 --- a/src/extension/common/variables/environment.ts +++ b/src/extension/common/variables/environment.ts @@ -104,7 +104,6 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria for (let i = 0; i < content.length; i++) { const char = content[i]; - const prevChar = i > 0 ? content[i - 1] : ''; // Track if we've seen an '=' sign (indicating we're in the value part) if (char === '=' && !inQuotes) { @@ -113,16 +112,29 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria continue; } - // Handle quote characters - if ((char === '"' || char === "'") && afterEquals && prevChar !== '\\') { - if (!inQuotes) { - // Starting a quoted section - inQuotes = true; - quoteChar = char; - } else if (char === quoteChar) { - // Ending a quoted section - inQuotes = false; - quoteChar = ''; + // 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; From 4bafda095c5596576c6d1810be942c76774dd6e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:21:18 +0000 Subject: [PATCH 4/5] Update comment for regex 's' flag usage Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- src/extension/common/variables/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/common/variables/environment.ts b/src/extension/common/variables/environment.ts index 30b0157f..e2d6b24a 100644 --- a/src/extension/common/variables/environment.ts +++ b/src/extension/common/variables/environment.ts @@ -176,7 +176,7 @@ 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. - // Modified to handle multiline values by using 's' flag to make . match newlines + // 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 ['', '']; From e4a6a92c39ebdde16d68ee0d9aec3c8ee307e6df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:45:23 +0000 Subject: [PATCH 5/5] Fix linter and prettier formatting issues Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com> --- src/extension/common/variables/environment.ts | 18 +++++++------- .../unittest/common/environment.unit.test.ts | 24 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/extension/common/variables/environment.ts b/src/extension/common/variables/environment.ts index e2d6b24a..ce67bb8b 100644 --- a/src/extension/common/variables/environment.ts +++ b/src/extension/common/variables/environment.ts @@ -95,23 +95,23 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria const globalVars = baseVars ? baseVars : {}; const vars: EnvironmentVariables = {}; const content = lines.toString(); - + // 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 @@ -121,10 +121,10 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria 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 @@ -139,7 +139,7 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria currentLine += char; continue; } - + // Handle newlines if (char === '\n') { if (inQuotes) { @@ -159,7 +159,7 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria currentLine += char; } } - + // Handle the last line if there's no trailing newline if (currentLine.trim() !== '') { const [name, value] = parseEnvLine(currentLine); @@ -167,7 +167,7 @@ export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVaria vars[name] = substituteEnvVars(value, vars, globalVars); } } - + return vars; } diff --git a/src/test/unittest/common/environment.unit.test.ts b/src/test/unittest/common/environment.unit.test.ts index a3a9e983..bf1e8cf7 100644 --- a/src/test/unittest/common/environment.unit.test.ts +++ b/src/test/unittest/common/environment.unit.test.ts @@ -10,7 +10,7 @@ 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', @@ -21,35 +21,35 @@ suite('Environment File Parsing Tests', () => { 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'); }); @@ -57,21 +57,21 @@ suite('Environment File Parsing Tests', () => { 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', @@ -82,21 +82,21 @@ suite('Environment File Parsing Tests', () => { 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'); });