diff --git a/README.md b/README.md index fdebdc73..dd80a01f 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,19 @@ To make VS Code map the files on the server to the right files on your local mac } ``` +### Environment Variables in Path Mappings + +You can use environment variables in `pathMappings` using the `${env:VARIABLE_NAME}` syntax. This is useful for dynamic paths that vary between environments or users: + +```json +"pathMappings": { + "${env:DOCKER_WEB_ROOT}": "${workspaceFolder}", + "/app": "${env:PROJECT_PATH}" +} +``` + +The environment variables are resolved when the debug session starts. If a variable is not defined, the literal string (e.g., `${env:UNDEFINED_VAR}`) will be kept as-is. + Please also note that setting any of the CLI debugging options will not work with remote host debugging, because the script is always launched locally. If you want to debug a CLI script on a remote host, you need to launch it manually from the command line. ## Proxy support diff --git a/src/envResolver.ts b/src/envResolver.ts new file mode 100644 index 00000000..8e4c70b9 --- /dev/null +++ b/src/envResolver.ts @@ -0,0 +1,11 @@ +/** + * Resolves environment variables in a string + * Supports: ${env:VAR_NAME} + */ +export function resolveEnvVariables(value: string): string { + // Replace ${env:VAR_NAME} with environment variable values + return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar: string) => { + const envValue = process.env[envVar] + return envValue !== undefined ? envValue : match + }) +} diff --git a/src/extension.ts b/src/extension.ts index 0838b3b0..755dc97b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import { WorkspaceFolder, DebugConfiguration, CancellationToken } from 'vscode' import { EvaluateExtendedArguments, LaunchRequestArguments } from './phpDebug' import * as which from 'which' import * as path from 'path' +import { resolveEnvVariables } from './envResolver' import { DebugProtocol } from '@vscode/debugprotocol' export function activate(context: vscode.ExtensionContext) { @@ -72,29 +73,32 @@ export function activate(context: vscode.ExtensionContext) { } } } - if (folder && folder.uri.scheme !== 'file') { - // replace - if (debugConfiguration.pathMappings) { - for (const key in debugConfiguration.pathMappings) { - debugConfiguration.pathMappings[key] = debugConfiguration.pathMappings[key].replace( - '${workspaceFolder}', - folder.uri.toString() - ) + if (debugConfiguration.pathMappings) { + const resolvedMappings: { [index: string]: string } = {} + for (const [serverPath, localPath] of Object.entries(debugConfiguration.pathMappings)) { + const resolvedServerPath = resolveEnvVariables(serverPath) + let resolvedLocalPath = resolveEnvVariables(localPath) + + if (folder && folder.uri.scheme !== 'file') { + resolvedLocalPath = resolvedLocalPath.replace('${workspaceFolder}', folder.uri.toString()) } + + resolvedMappings[resolvedServerPath] = resolvedLocalPath } - // The following path are currently NOT mapped - /* - debugConfiguration.skipEntryPaths = debugConfiguration.skipEntryPaths?.map(v => - v.replace('${workspaceFolder}', folder.uri.toString()) - ) - debugConfiguration.skipFiles = debugConfiguration.skipFiles?.map(v => - v.replace('${workspaceFolder}', folder.uri.toString()) - ) - debugConfiguration.ignore = debugConfiguration.ignore?.map(v => - v.replace('${workspaceFolder}', folder.uri.toString()) - ) - */ + debugConfiguration.pathMappings = resolvedMappings } + // The following path are currently NOT mapped + /* + debugConfiguration.skipEntryPaths = debugConfiguration.skipEntryPaths?.map(v => + v.replace('${workspaceFolder}', folder.uri.toString()) + ) + debugConfiguration.skipFiles = debugConfiguration.skipFiles?.map(v => + v.replace('${workspaceFolder}', folder.uri.toString()) + ) + debugConfiguration.ignore = debugConfiguration.ignore?.map(v => + v.replace('${workspaceFolder}', folder.uri.toString()) + ) + */ return debugConfiguration }, }) diff --git a/src/test/envResolver.ts b/src/test/envResolver.ts new file mode 100644 index 00000000..1c3f47fd --- /dev/null +++ b/src/test/envResolver.ts @@ -0,0 +1,68 @@ +import { assert } from 'chai' +import { describe, it, beforeEach, afterEach } from 'mocha' +import { resolveEnvVariables } from '../envResolver' + +describe('Environment Variable Resolution', () => { + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + originalEnv = { ...process.env } + }) + + afterEach(() => { + process.env = originalEnv + }) + + it('should resolve ${env:VAR_NAME} with existing environment variable', () => { + process.env.TEST_VAR = '/test/path' + const result = resolveEnvVariables('${env:TEST_VAR}/subdir') + assert.equal(result, '/test/path/subdir') + }) + + it('should keep ${env:VAR_NAME} if environment variable does not exist', () => { + delete process.env.NONEXISTENT_VAR + const result = resolveEnvVariables('${env:NONEXISTENT_VAR}/subdir') + assert.equal(result, '${env:NONEXISTENT_VAR}/subdir') + }) + + it('should resolve multiple environment variables', () => { + process.env.VAR1 = '/path1' + process.env.VAR2 = '/path2' + const result = resolveEnvVariables('${env:VAR1}/${env:VAR2}') + assert.equal(result, '/path1//path2') + }) + + it('should handle text without environment variables', () => { + const result = resolveEnvVariables('/var/www/html') + assert.equal(result, '/var/www/html') + }) + + it('should handle environment variables with underscores and numbers', () => { + process.env.MY_VAR_123 = '/custom/path' + const result = resolveEnvVariables('${env:MY_VAR_123}') + assert.equal(result, '/custom/path') + }) + + it('should handle empty environment variable value', () => { + process.env.EMPTY_VAR = '' + const result = resolveEnvVariables('${env:EMPTY_VAR}/test') + assert.equal(result, '/test') + }) + + it('should handle mixed content', () => { + process.env.DOCKER_ROOT = '/var/www/html' + const result = resolveEnvVariables('prefix/${env:DOCKER_ROOT}/suffix') + assert.equal(result, 'prefix//var/www/html/suffix') + }) + + it('should handle pathMapping use case', () => { + process.env.DOCKER_WEB_ROOT = '/var/www/html' + process.env.LOCAL_PROJECT = '/Users/developer/myproject' + + const serverPath = '${env:DOCKER_WEB_ROOT}' + const localPath = '${env:LOCAL_PROJECT}' + + assert.equal(resolveEnvVariables(serverPath), '/var/www/html') + assert.equal(resolveEnvVariables(localPath), '/Users/developer/myproject') + }) +})