From 2ca05654c0a0b38383350bad38d4c4b8ba129bd8 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Mon, 8 Dec 2025 16:31:22 -0800 Subject: [PATCH 1/3] Add terminalQuoteCharacter as an option --- contributing.md | 2 +- package.json | 36 +++++++++++++ .../configuration/resolvers/launch.ts | 52 ++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/contributing.md b/contributing.md index a4d1f4a9..1d67e193 100644 --- a/contributing.md +++ b/contributing.md @@ -19,7 +19,7 @@ source .venv/bin/activate Install then setup with nox. ``` python3 -m pip install nox -nox --session setup_repo +nox ``` ## Reporting Issues diff --git a/package.json b/package.json index f003234b..83484a55 100644 --- a/package.json +++ b/package.json @@ -256,6 +256,24 @@ "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.", "type": "boolean" }, + "terminalQuoteCharacter": { + "default": null, + "description": "The quoting character to be used by the debugger when quoting terminal commands.", + "type": [ + "string", + "null" + ], + "enum": [ + "\"", + "'", + "`" + ], + "enumDescriptions": [ + "Double quote (\")", + "Single quote (')", + "Backtick (`)" + ] + }, "pathMappings": { "default": [], "items": { @@ -452,6 +470,24 @@ "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.", "type": "boolean" }, + "terminalQuoteCharacter": { + "default": null, + "description": "The quoting character to be used by the debugger when quoting terminal commands.", + "type": [ + "string", + "null" + ], + "enum": [ + "\"", + "'", + "`" + ], + "enumDescriptions": [ + "Double quote (\")", + "Single quote (')", + "Backtick (`)" + ] + }, "module": { "default": "", "description": "Name of the module to be debugged.", diff --git a/src/extension/debugger/configuration/resolvers/launch.ts b/src/extension/debugger/configuration/resolvers/launch.ts index 89efe76a..6dcf25d1 100644 --- a/src/extension/debugger/configuration/resolvers/launch.ts +++ b/src/extension/debugger/configuration/resolvers/launch.ts @@ -3,7 +3,7 @@ 'use strict'; -import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken, Uri, WorkspaceFolder, workspace } from 'vscode'; import { getOSType, OSType } from '../../../common/platform'; import { getEnvFile } from '../../../common/settings'; import { DebuggerTypeName } from '../../../constants'; @@ -13,6 +13,7 @@ import { BaseConfigurationResolver } from './base'; import { getDebugEnvironmentVariables, getProgram } from './helper'; import { getConfiguration } from '../../../common/vscodeapi'; import { traceLog } from '../../../common/log/logging'; +import { debug } from 'console'; export class LaunchConfigurationResolver extends BaseConfigurationResolver { public async resolveDebugConfiguration( @@ -105,6 +106,13 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver('integrated.defaultProfile.windows'); + profiles = config.get('integrated.profiles.windows'); + } else if (platform === 'linux') { + defaultProfile = config.get('integrated.defaultProfile.linux'); + profiles = config.get('integrated.profiles.linux'); + } else if (platform === 'darwin') { + defaultProfile = config.get('integrated.defaultProfile.osx'); + profiles = config.get('integrated.profiles.osx'); + } + + if (!defaultProfile || !profiles) { + if (platform === 'win32') { + return "'"; // Default is powershell + } else { + return '"'; // Default is bash/zsh + } + } + + const profile = defaultProfile ? profiles[defaultProfile] : profiles[0]; + const shellPath = profile?.path || ''; + + if (/powershell/i.test(shellPath)) { + return "'"; + } + if (/cmd\.exe$/i.test(shellPath)) { + return '"'; + } + if (/bash|zsh|fish/i.test(shellPath)) { + return '"'; + } + + return '"'; + } } From 70d3367ad85933a374c98bf878fffc89a05b685f Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Tue, 9 Dec 2025 09:22:05 -0800 Subject: [PATCH 2/3] Add unit tests --- .../configuration/resolvers/launch.ts | 7 +- .../resolvers/launch.unit.test.ts | 227 ++++++++++++++++++ 2 files changed, 230 insertions(+), 4 deletions(-) diff --git a/src/extension/debugger/configuration/resolvers/launch.ts b/src/extension/debugger/configuration/resolvers/launch.ts index 6dcf25d1..e991826f 100644 --- a/src/extension/debugger/configuration/resolvers/launch.ts +++ b/src/extension/debugger/configuration/resolvers/launch.ts @@ -3,7 +3,7 @@ 'use strict'; -import { CancellationToken, Uri, WorkspaceFolder, workspace } from 'vscode'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; import { getOSType, OSType } from '../../../common/platform'; import { getEnvFile } from '../../../common/settings'; import { DebuggerTypeName } from '../../../constants'; @@ -13,7 +13,6 @@ import { BaseConfigurationResolver } from './base'; import { getDebugEnvironmentVariables, getProgram } from './helper'; import { getConfiguration } from '../../../common/vscodeapi'; import { traceLog } from '../../../common/log/logging'; -import { debug } from 'console'; export class LaunchConfigurationResolver extends BaseConfigurationResolver { public async resolveDebugConfiguration( @@ -193,7 +192,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { getDebugEnvironmentVariablesStub = sinon.stub(helper, 'getDebugEnvironmentVariables'); getConfigurationStub = sinon.stub(vscodeapi, 'getConfiguration'); getConfigurationStub.withArgs('debugpy', sinon.match.any).returns(createMoqConfiguration(true)); + // Mock python configuration for useEnvExtension check + const pythonConfig = TypeMoq.Mock.ofType(); + pythonConfig.setup((p) => p.get('useEnvironmentsExtension', false)).returns(() => false); + getConfigurationStub.withArgs('python').returns(pythonConfig.object); }); teardown(() => { @@ -1045,5 +1049,228 @@ getInfoPerOS().forEach(([osName, osType, path]) => { await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); }); }); + + suite('terminalQuoteCharacter tests', () => { + let platformStub: sinon.SinonStub; + + function setupTerminalProfile( + profile: string | undefined, + path: string = 'bash', + platform: NodeJS.Platform = 'linux', + ) { + const terminalConfig = TypeMoq.Mock.ofType(); + + // Mock process.platform + if (platformStub) { + platformStub.restore(); + } + platformStub = sinon.stub(process, 'platform').value(platform); + + if (platform === 'win32') { + terminalConfig + .setup((c) => c.get('integrated.defaultProfile.windows')) + .returns(() => profile); + const profiles: any = {}; + if (profile) { + profiles[profile] = { path }; + } + terminalConfig.setup((c) => c.get('integrated.profiles.windows')).returns(() => profiles); + } else if (platform === 'linux') { + terminalConfig + .setup((c) => c.get('integrated.defaultProfile.linux')) + .returns(() => profile); + const profiles: any = {}; + if (profile) { + profiles[profile] = { path }; + } + terminalConfig.setup((c) => c.get('integrated.profiles.linux')).returns(() => profiles); + } else if (platform === 'darwin') { + terminalConfig.setup((c) => c.get('integrated.defaultProfile.osx')).returns(() => profile); + const profiles: any = {}; + if (profile) { + profiles[profile] = { path }; + } + terminalConfig.setup((c) => c.get('integrated.profiles.osx')).returns(() => profiles); + } + + getConfigurationStub.withArgs('terminal').returns(terminalConfig.object); + } + + test('Default terminalQuoteCharacter is computed for bash shell', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('Bash', '/bin/bash', 'linux'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', '"'); + }); + + test('Default terminalQuoteCharacter is computed for zsh shell', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('Zsh', '/bin/zsh', 'darwin'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', '"'); + }); + + test('Default terminalQuoteCharacter is computed for fish shell', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('Fish', '/usr/bin/fish', 'linux'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', '"'); + }); + + test('Default terminalQuoteCharacter is computed for PowerShell', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile( + 'PowerShell', + 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', + 'win32', + ); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', "'"); + }); + + test('Default terminalQuoteCharacter is computed for cmd.exe', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('Command Prompt', 'C:\\Windows\\System32\\cmd.exe', 'win32'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', '"'); + }); + + test('Default terminalQuoteCharacter is double quote when terminal profile is not configured', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile(undefined, 'bash', 'linux'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', '"'); + }); + + test('User-provided terminalQuoteCharacter is preserved when valid (single character)', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('Bash', '/bin/bash', 'linux'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + terminalQuoteCharacter: "'", + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', "'"); + }); + + test('Invalid terminalQuoteCharacter (empty string) triggers fallback to computed default', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('PowerShell', 'powershell.exe', 'win32'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + terminalQuoteCharacter: '', + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', "'"); + }); + + test('Invalid terminalQuoteCharacter (multi-character) triggers fallback to computed default', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('Bash', '/bin/bash', 'linux'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + terminalQuoteCharacter: '""', + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', '"'); + }); + + test('terminalQuoteCharacter handles pwsh (PowerShell Core)', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile('PowerShell Core', '/usr/local/bin/pwsh', 'darwin'); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', "'"); + }); + + test('terminalQuoteCharacter is case-insensitive for shell detection', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupTerminalProfile( + 'PowerShell', + 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\POWERSHELL.EXE', + 'win32', + ); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + }); + + expect(debugConfig).to.have.property('terminalQuoteCharacter', "'"); + }); + }); }); }); From 1c78e3b9c4c468f6d2460ae14617718ac51227e3 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Tue, 9 Dec 2025 09:42:56 -0800 Subject: [PATCH 3/3] Fix other launch unit tests --- .../unittest/configuration/resolvers/launch.unit.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/unittest/configuration/resolvers/launch.unit.test.ts b/src/test/unittest/configuration/resolvers/launch.unit.test.ts index 0a6249c9..d8b46eda 100644 --- a/src/test/unittest/configuration/resolvers/launch.unit.test.ts +++ b/src/test/unittest/configuration/resolvers/launch.unit.test.ts @@ -56,6 +56,11 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const pythonConfig = TypeMoq.Mock.ofType(); pythonConfig.setup((p) => p.get('useEnvironmentsExtension', false)).returns(() => false); getConfigurationStub.withArgs('python').returns(pythonConfig.object); + // Mock terminal configuration with default values + const terminalConfig = TypeMoq.Mock.ofType(); + terminalConfig.setup((c) => c.get(TypeMoq.It.isAnyString())).returns(() => undefined); + terminalConfig.setup((c) => c.get(TypeMoq.It.isAnyString())).returns(() => undefined); + getConfigurationStub.withArgs('terminal').returns(terminalConfig.object); }); teardown(() => {