Skip to content

Commit 6a30b35

Browse files
committed
Fix environment variable substitution in .env files
- Merge process.env with debugLaunchEnvVars before parsing .env file - This ensures system environment variables like PATH are available for substitution - Add comprehensive unit tests for environment variable substitution - Fixes issue where ${PATH} and other system vars weren't expanded in .env files
1 parent a8d6cc5 commit 6a30b35

File tree

2 files changed

+168
-1
lines changed

2 files changed

+168
-1
lines changed

src/extension/debugger/configuration/resolvers/helper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export async function getDebugEnvironmentVariables(args: LaunchRequestArguments)
1919
args.env && Object.keys(args.env).length > 0
2020
? ({ ...args.env } as Record<string, string>)
2121
: ({} as Record<string, string>);
22-
const envFileVars = await envParser.parseFile(args.envFile, debugLaunchEnvVars);
22+
// Merge process.env with debugLaunchEnvVars for variable substitution in .env file
23+
const baseVars = { ...process.env, ...debugLaunchEnvVars };
24+
const envFileVars = await envParser.parseFile(args.envFile, baseVars);
2325
const env = envFileVars ? { ...envFileVars } : {};
2426

2527
// "overwrite: true" to ensure that debug-configuration env variable values
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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 * as sinon from 'sinon';
8+
import * as envParser from '../../../../extension/common/variables/environment';
9+
import { getDebugEnvironmentVariables } from '../../../../extension/debugger/configuration/resolvers/helper';
10+
import { LaunchRequestArguments } from '../../../../extension/types';
11+
12+
suite('Debugging - Environment Variable Substitution', () => {
13+
let parseFileStub: sinon.SinonStub;
14+
15+
setup(() => {
16+
parseFileStub = sinon.stub(envParser, 'parseFile');
17+
sinon.stub(envParser, 'mergeVariables');
18+
sinon.stub(envParser, 'appendPath');
19+
sinon.stub(envParser, 'appendPythonPath');
20+
});
21+
22+
teardown(() => {
23+
sinon.restore();
24+
});
25+
26+
test('Environment variables from process.env should be available for substitution in .env file', async () => {
27+
// Arrange
28+
const args: LaunchRequestArguments = {
29+
name: 'Test',
30+
type: 'debugpy',
31+
request: 'launch',
32+
envFile: '/path/to/.env',
33+
env: {
34+
CUSTOM_VAR: 'custom_value',
35+
},
36+
};
37+
38+
// Set up process.env
39+
const originalPath = process.env.PATH;
40+
process.env.PATH = '/usr/bin:/usr/local/bin';
41+
42+
parseFileStub.resolves({
43+
EXPANDED_PATH: '${PATH}:/my/custom/path',
44+
});
45+
46+
// Act
47+
await getDebugEnvironmentVariables(args);
48+
49+
// Assert
50+
// Verify that parseFile was called with merged environment variables
51+
expect(parseFileStub.calledOnce).to.be.true;
52+
const [envFilePath, baseVars] = parseFileStub.firstCall.args;
53+
expect(envFilePath).to.equal('/path/to/.env');
54+
expect(baseVars).to.have.property('PATH', '/usr/bin:/usr/local/bin');
55+
expect(baseVars).to.have.property('CUSTOM_VAR', 'custom_value');
56+
57+
// Restore process.env
58+
if (originalPath !== undefined) {
59+
process.env.PATH = originalPath;
60+
}
61+
});
62+
63+
test('Debug launch env vars should override process.env during substitution', async () => {
64+
// Arrange
65+
const args: LaunchRequestArguments = {
66+
name: 'Test',
67+
type: 'debugpy',
68+
request: 'launch',
69+
envFile: '/path/to/.env',
70+
env: {
71+
PATH: '/custom/path',
72+
MY_VAR: 'my_value',
73+
},
74+
};
75+
76+
// Set up process.env
77+
const originalPath = process.env.PATH;
78+
process.env.PATH = '/usr/bin:/usr/local/bin';
79+
process.env.MY_VAR = 'system_value';
80+
81+
parseFileStub.resolves({});
82+
83+
// Act
84+
await getDebugEnvironmentVariables(args);
85+
86+
// Assert
87+
expect(parseFileStub.calledOnce).to.be.true;
88+
const [, baseVars] = parseFileStub.firstCall.args;
89+
// Debug launch vars should override process.env
90+
expect(baseVars).to.have.property('PATH', '/custom/path');
91+
expect(baseVars).to.have.property('MY_VAR', 'my_value');
92+
93+
// Restore process.env
94+
if (originalPath !== undefined) {
95+
process.env.PATH = originalPath;
96+
}
97+
delete process.env.MY_VAR;
98+
});
99+
100+
test('All process.env variables should be available for substitution', async () => {
101+
// Arrange
102+
const args: LaunchRequestArguments = {
103+
name: 'Test',
104+
type: 'debugpy',
105+
request: 'launch',
106+
envFile: '/path/to/.env',
107+
env: {},
108+
};
109+
110+
// Set up process.env
111+
const originalEnv = { ...process.env };
112+
process.env.TEST_VAR_1 = 'value1';
113+
process.env.TEST_VAR_2 = 'value2';
114+
process.env.PATH = '/test/path';
115+
116+
parseFileStub.resolves({});
117+
118+
// Act
119+
await getDebugEnvironmentVariables(args);
120+
121+
// Assert
122+
expect(parseFileStub.calledOnce).to.be.true;
123+
const [, baseVars] = parseFileStub.firstCall.args;
124+
expect(baseVars).to.have.property('TEST_VAR_1', 'value1');
125+
expect(baseVars).to.have.property('TEST_VAR_2', 'value2');
126+
expect(baseVars).to.have.property('PATH', '/test/path');
127+
128+
// Restore process.env
129+
Object.keys(process.env).forEach((key) => {
130+
if (!(key in originalEnv)) {
131+
delete process.env[key];
132+
}
133+
});
134+
Object.assign(process.env, originalEnv);
135+
});
136+
137+
test('Empty env in launch config should still provide process.env for substitution', async () => {
138+
// Arrange
139+
const args: LaunchRequestArguments = {
140+
name: 'Test',
141+
type: 'debugpy',
142+
request: 'launch',
143+
envFile: '/path/to/.env',
144+
// No env property
145+
};
146+
147+
const originalPath = process.env.PATH;
148+
process.env.PATH = '/usr/bin';
149+
150+
parseFileStub.resolves({});
151+
152+
// Act
153+
await getDebugEnvironmentVariables(args);
154+
155+
// Assert
156+
expect(parseFileStub.calledOnce).to.be.true;
157+
const [, baseVars] = parseFileStub.firstCall.args;
158+
expect(baseVars).to.have.property('PATH', '/usr/bin');
159+
160+
// Restore
161+
if (originalPath !== undefined) {
162+
process.env.PATH = originalPath;
163+
}
164+
});
165+
});

0 commit comments

Comments
 (0)