Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.",
Expand Down
49 changes: 49 additions & 0 deletions src/extension/debugger/configuration/resolvers/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
if (debugConfiguration.console !== 'internalConsole' && !debugConfiguration.internalConsoleOptions) {
debugConfiguration.internalConsoleOptions = 'neverOpen';
}

// Compute the terminal quoting character.
if (!debugConfiguration.terminalQuoteCharacter || debugConfiguration.terminalQuoteCharacter.length !== 1) {
const quoteChar = this._computeTerminalQuoteCharacter();
debugConfiguration.terminalQuoteCharacter = quoteChar;
}

if (!Array.isArray(debugConfiguration.debugOptions)) {
debugConfiguration.debugOptions = [];
}
Expand Down Expand Up @@ -182,4 +189,46 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
: 'launch';
LaunchConfigurationResolver.sendTelemetry(trigger, debugConfiguration);
}

private _computeTerminalQuoteCharacter(): string {
const platform = process.platform; // 'win32', 'linux', 'darwin'
const config = getConfiguration('terminal');

let defaultProfile: string | undefined;
let profiles: any;

if (platform === 'win32') {
defaultProfile = config.get<string>('integrated.defaultProfile.windows');
profiles = config.get<any>('integrated.profiles.windows');
} else if (platform === 'linux') {
defaultProfile = config.get<string>('integrated.defaultProfile.linux');
profiles = config.get<any>('integrated.profiles.linux');
} else if (platform === 'darwin') {
defaultProfile = config.get<string>('integrated.defaultProfile.osx');
profiles = config.get<any>('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|pwsh/i.test(shellPath)) {
return "'";
}
if (/cmd\.exe$/i.test(shellPath)) {
return '"';
}
if (/bash|zsh|fish/i.test(shellPath)) {
return '"';
}

return '"';
}
}
232 changes: 232 additions & 0 deletions src/test/unittest/configuration/resolvers/launch.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => {
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<WorkspaceConfiguration>();
pythonConfig.setup((p) => p.get<boolean>('useEnvironmentsExtension', false)).returns(() => false);
getConfigurationStub.withArgs('python').returns(pythonConfig.object);
// Mock terminal configuration with default values
const terminalConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
terminalConfig.setup((c) => c.get<string>(TypeMoq.It.isAnyString())).returns(() => undefined);
terminalConfig.setup((c) => c.get<any>(TypeMoq.It.isAnyString())).returns(() => undefined);
getConfigurationStub.withArgs('terminal').returns(terminalConfig.object);
});

teardown(() => {
Expand Down Expand Up @@ -1045,5 +1054,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<WorkspaceConfiguration>();

// Mock process.platform
if (platformStub) {
platformStub.restore();
}
platformStub = sinon.stub(process, 'platform').value(platform);

if (platform === 'win32') {
terminalConfig
.setup((c) => c.get<string>('integrated.defaultProfile.windows'))
.returns(() => profile);
const profiles: any = {};
if (profile) {
profiles[profile] = { path };
}
terminalConfig.setup((c) => c.get<any>('integrated.profiles.windows')).returns(() => profiles);
} else if (platform === 'linux') {
terminalConfig
.setup((c) => c.get<string>('integrated.defaultProfile.linux'))
.returns(() => profile);
const profiles: any = {};
if (profile) {
profiles[profile] = { path };
}
terminalConfig.setup((c) => c.get<any>('integrated.profiles.linux')).returns(() => profiles);
} else if (platform === 'darwin') {
terminalConfig.setup((c) => c.get<string>('integrated.defaultProfile.osx')).returns(() => profile);
const profiles: any = {};
if (profile) {
profiles[profile] = { path };
}
terminalConfig.setup((c) => c.get<any>('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', "'");
});
});
});
});
Loading