Skip to content

Commit 5a633be

Browse files
authored
Add 'terminalQuoteCharacter' to the debug configuration we send to debugpy (#886)
* Add terminalQuoteCharacter as an option * Add unit tests * Fix other launch unit tests
1 parent 1b01343 commit 5a633be

File tree

4 files changed

+318
-1
lines changed

4 files changed

+318
-1
lines changed

contributing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ source .venv/bin/activate
1919
Install then setup with nox.
2020
```
2121
python3 -m pip install nox
22-
nox --session setup_repo
22+
nox
2323
```
2424

2525
## Reporting Issues

package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,24 @@
256256
"description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
257257
"type": "boolean"
258258
},
259+
"terminalQuoteCharacter": {
260+
"default": null,
261+
"description": "The quoting character to be used by the debugger when quoting terminal commands.",
262+
"type": [
263+
"string",
264+
"null"
265+
],
266+
"enum": [
267+
"\"",
268+
"'",
269+
"`"
270+
],
271+
"enumDescriptions": [
272+
"Double quote (\")",
273+
"Single quote (')",
274+
"Backtick (`)"
275+
]
276+
},
259277
"pathMappings": {
260278
"default": [],
261279
"items": {
@@ -452,6 +470,24 @@
452470
"description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
453471
"type": "boolean"
454472
},
473+
"terminalQuoteCharacter": {
474+
"default": null,
475+
"description": "The quoting character to be used by the debugger when quoting terminal commands.",
476+
"type": [
477+
"string",
478+
"null"
479+
],
480+
"enum": [
481+
"\"",
482+
"'",
483+
"`"
484+
],
485+
"enumDescriptions": [
486+
"Double quote (\")",
487+
"Single quote (')",
488+
"Backtick (`)"
489+
]
490+
},
455491
"module": {
456492
"default": "",
457493
"description": "Name of the module to be debugged.",

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
105105
if (debugConfiguration.console !== 'internalConsole' && !debugConfiguration.internalConsoleOptions) {
106106
debugConfiguration.internalConsoleOptions = 'neverOpen';
107107
}
108+
109+
// Compute the terminal quoting character.
110+
if (!debugConfiguration.terminalQuoteCharacter || debugConfiguration.terminalQuoteCharacter.length !== 1) {
111+
const quoteChar = this._computeTerminalQuoteCharacter();
112+
debugConfiguration.terminalQuoteCharacter = quoteChar;
113+
}
114+
108115
if (!Array.isArray(debugConfiguration.debugOptions)) {
109116
debugConfiguration.debugOptions = [];
110117
}
@@ -182,4 +189,46 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
182189
: 'launch';
183190
LaunchConfigurationResolver.sendTelemetry(trigger, debugConfiguration);
184191
}
192+
193+
private _computeTerminalQuoteCharacter(): string {
194+
const platform = process.platform; // 'win32', 'linux', 'darwin'
195+
const config = getConfiguration('terminal');
196+
197+
let defaultProfile: string | undefined;
198+
let profiles: any;
199+
200+
if (platform === 'win32') {
201+
defaultProfile = config.get<string>('integrated.defaultProfile.windows');
202+
profiles = config.get<any>('integrated.profiles.windows');
203+
} else if (platform === 'linux') {
204+
defaultProfile = config.get<string>('integrated.defaultProfile.linux');
205+
profiles = config.get<any>('integrated.profiles.linux');
206+
} else if (platform === 'darwin') {
207+
defaultProfile = config.get<string>('integrated.defaultProfile.osx');
208+
profiles = config.get<any>('integrated.profiles.osx');
209+
}
210+
211+
if (!defaultProfile || !profiles) {
212+
if (platform === 'win32') {
213+
return "'"; // Default is powershell
214+
} else {
215+
return '"'; // Default is bash/zsh
216+
}
217+
}
218+
219+
const profile = defaultProfile ? profiles[defaultProfile] : profiles[0];
220+
const shellPath = profile?.path || '';
221+
222+
if (/powershell|pwsh/i.test(shellPath)) {
223+
return "'";
224+
}
225+
if (/cmd\.exe$/i.test(shellPath)) {
226+
return '"';
227+
}
228+
if (/bash|zsh|fish/i.test(shellPath)) {
229+
return '"';
230+
}
231+
232+
return '"';
233+
}
185234
}

src/test/unittest/configuration/resolvers/launch.unit.test.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ getInfoPerOS().forEach(([osName, osType, path]) => {
5252
getDebugEnvironmentVariablesStub = sinon.stub(helper, 'getDebugEnvironmentVariables');
5353
getConfigurationStub = sinon.stub(vscodeapi, 'getConfiguration');
5454
getConfigurationStub.withArgs('debugpy', sinon.match.any).returns(createMoqConfiguration(true));
55+
// Mock python configuration for useEnvExtension check
56+
const pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
57+
pythonConfig.setup((p) => p.get<boolean>('useEnvironmentsExtension', false)).returns(() => false);
58+
getConfigurationStub.withArgs('python').returns(pythonConfig.object);
59+
// Mock terminal configuration with default values
60+
const terminalConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
61+
terminalConfig.setup((c) => c.get<string>(TypeMoq.It.isAnyString())).returns(() => undefined);
62+
terminalConfig.setup((c) => c.get<any>(TypeMoq.It.isAnyString())).returns(() => undefined);
63+
getConfigurationStub.withArgs('terminal').returns(terminalConfig.object);
5564
});
5665

5766
teardown(() => {
@@ -1045,5 +1054,228 @@ getInfoPerOS().forEach(([osName, osType, path]) => {
10451054
await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true);
10461055
});
10471056
});
1057+
1058+
suite('terminalQuoteCharacter tests', () => {
1059+
let platformStub: sinon.SinonStub;
1060+
1061+
function setupTerminalProfile(
1062+
profile: string | undefined,
1063+
path: string = 'bash',
1064+
platform: NodeJS.Platform = 'linux',
1065+
) {
1066+
const terminalConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
1067+
1068+
// Mock process.platform
1069+
if (platformStub) {
1070+
platformStub.restore();
1071+
}
1072+
platformStub = sinon.stub(process, 'platform').value(platform);
1073+
1074+
if (platform === 'win32') {
1075+
terminalConfig
1076+
.setup((c) => c.get<string>('integrated.defaultProfile.windows'))
1077+
.returns(() => profile);
1078+
const profiles: any = {};
1079+
if (profile) {
1080+
profiles[profile] = { path };
1081+
}
1082+
terminalConfig.setup((c) => c.get<any>('integrated.profiles.windows')).returns(() => profiles);
1083+
} else if (platform === 'linux') {
1084+
terminalConfig
1085+
.setup((c) => c.get<string>('integrated.defaultProfile.linux'))
1086+
.returns(() => profile);
1087+
const profiles: any = {};
1088+
if (profile) {
1089+
profiles[profile] = { path };
1090+
}
1091+
terminalConfig.setup((c) => c.get<any>('integrated.profiles.linux')).returns(() => profiles);
1092+
} else if (platform === 'darwin') {
1093+
terminalConfig.setup((c) => c.get<string>('integrated.defaultProfile.osx')).returns(() => profile);
1094+
const profiles: any = {};
1095+
if (profile) {
1096+
profiles[profile] = { path };
1097+
}
1098+
terminalConfig.setup((c) => c.get<any>('integrated.profiles.osx')).returns(() => profiles);
1099+
}
1100+
1101+
getConfigurationStub.withArgs('terminal').returns(terminalConfig.object);
1102+
}
1103+
1104+
test('Default terminalQuoteCharacter is computed for bash shell', async () => {
1105+
const pythonPath = `PythonPath_${new Date().toString()}`;
1106+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1107+
const pythonFile = 'xyz.py';
1108+
setupIoc(pythonPath, workspaceFolder);
1109+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1110+
setupTerminalProfile('Bash', '/bin/bash', 'linux');
1111+
1112+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1113+
...launch,
1114+
});
1115+
1116+
expect(debugConfig).to.have.property('terminalQuoteCharacter', '"');
1117+
});
1118+
1119+
test('Default terminalQuoteCharacter is computed for zsh shell', async () => {
1120+
const pythonPath = `PythonPath_${new Date().toString()}`;
1121+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1122+
const pythonFile = 'xyz.py';
1123+
setupIoc(pythonPath, workspaceFolder);
1124+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1125+
setupTerminalProfile('Zsh', '/bin/zsh', 'darwin');
1126+
1127+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1128+
...launch,
1129+
});
1130+
1131+
expect(debugConfig).to.have.property('terminalQuoteCharacter', '"');
1132+
});
1133+
1134+
test('Default terminalQuoteCharacter is computed for fish shell', async () => {
1135+
const pythonPath = `PythonPath_${new Date().toString()}`;
1136+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1137+
const pythonFile = 'xyz.py';
1138+
setupIoc(pythonPath, workspaceFolder);
1139+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1140+
setupTerminalProfile('Fish', '/usr/bin/fish', 'linux');
1141+
1142+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1143+
...launch,
1144+
});
1145+
1146+
expect(debugConfig).to.have.property('terminalQuoteCharacter', '"');
1147+
});
1148+
1149+
test('Default terminalQuoteCharacter is computed for PowerShell', async () => {
1150+
const pythonPath = `PythonPath_${new Date().toString()}`;
1151+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1152+
const pythonFile = 'xyz.py';
1153+
setupIoc(pythonPath, workspaceFolder);
1154+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1155+
setupTerminalProfile(
1156+
'PowerShell',
1157+
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
1158+
'win32',
1159+
);
1160+
1161+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1162+
...launch,
1163+
});
1164+
1165+
expect(debugConfig).to.have.property('terminalQuoteCharacter', "'");
1166+
});
1167+
1168+
test('Default terminalQuoteCharacter is computed for cmd.exe', async () => {
1169+
const pythonPath = `PythonPath_${new Date().toString()}`;
1170+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1171+
const pythonFile = 'xyz.py';
1172+
setupIoc(pythonPath, workspaceFolder);
1173+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1174+
setupTerminalProfile('Command Prompt', 'C:\\Windows\\System32\\cmd.exe', 'win32');
1175+
1176+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1177+
...launch,
1178+
});
1179+
1180+
expect(debugConfig).to.have.property('terminalQuoteCharacter', '"');
1181+
});
1182+
1183+
test('Default terminalQuoteCharacter is double quote when terminal profile is not configured', async () => {
1184+
const pythonPath = `PythonPath_${new Date().toString()}`;
1185+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1186+
const pythonFile = 'xyz.py';
1187+
setupIoc(pythonPath, workspaceFolder);
1188+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1189+
setupTerminalProfile(undefined, 'bash', 'linux');
1190+
1191+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1192+
...launch,
1193+
});
1194+
1195+
expect(debugConfig).to.have.property('terminalQuoteCharacter', '"');
1196+
});
1197+
1198+
test('User-provided terminalQuoteCharacter is preserved when valid (single character)', async () => {
1199+
const pythonPath = `PythonPath_${new Date().toString()}`;
1200+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1201+
const pythonFile = 'xyz.py';
1202+
setupIoc(pythonPath, workspaceFolder);
1203+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1204+
setupTerminalProfile('Bash', '/bin/bash', 'linux');
1205+
1206+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1207+
...launch,
1208+
terminalQuoteCharacter: "'",
1209+
});
1210+
1211+
expect(debugConfig).to.have.property('terminalQuoteCharacter', "'");
1212+
});
1213+
1214+
test('Invalid terminalQuoteCharacter (empty string) triggers fallback to computed default', async () => {
1215+
const pythonPath = `PythonPath_${new Date().toString()}`;
1216+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1217+
const pythonFile = 'xyz.py';
1218+
setupIoc(pythonPath, workspaceFolder);
1219+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1220+
setupTerminalProfile('PowerShell', 'powershell.exe', 'win32');
1221+
1222+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1223+
...launch,
1224+
terminalQuoteCharacter: '',
1225+
});
1226+
1227+
expect(debugConfig).to.have.property('terminalQuoteCharacter', "'");
1228+
});
1229+
1230+
test('Invalid terminalQuoteCharacter (multi-character) triggers fallback to computed default', async () => {
1231+
const pythonPath = `PythonPath_${new Date().toString()}`;
1232+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1233+
const pythonFile = 'xyz.py';
1234+
setupIoc(pythonPath, workspaceFolder);
1235+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1236+
setupTerminalProfile('Bash', '/bin/bash', 'linux');
1237+
1238+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1239+
...launch,
1240+
terminalQuoteCharacter: '""',
1241+
});
1242+
1243+
expect(debugConfig).to.have.property('terminalQuoteCharacter', '"');
1244+
});
1245+
1246+
test('terminalQuoteCharacter handles pwsh (PowerShell Core)', async () => {
1247+
const pythonPath = `PythonPath_${new Date().toString()}`;
1248+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1249+
const pythonFile = 'xyz.py';
1250+
setupIoc(pythonPath, workspaceFolder);
1251+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1252+
setupTerminalProfile('PowerShell Core', '/usr/local/bin/pwsh', 'darwin');
1253+
1254+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1255+
...launch,
1256+
});
1257+
1258+
expect(debugConfig).to.have.property('terminalQuoteCharacter', "'");
1259+
});
1260+
1261+
test('terminalQuoteCharacter is case-insensitive for shell detection', async () => {
1262+
const pythonPath = `PythonPath_${new Date().toString()}`;
1263+
const workspaceFolder = createMoqWorkspaceFolder(__dirname);
1264+
const pythonFile = 'xyz.py';
1265+
setupIoc(pythonPath, workspaceFolder);
1266+
setupActiveEditor(pythonFile, PYTHON_LANGUAGE);
1267+
setupTerminalProfile(
1268+
'PowerShell',
1269+
'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\POWERSHELL.EXE',
1270+
'win32',
1271+
);
1272+
1273+
const debugConfig = await resolveDebugConfiguration(workspaceFolder, {
1274+
...launch,
1275+
});
1276+
1277+
expect(debugConfig).to.have.property('terminalQuoteCharacter', "'");
1278+
});
1279+
});
10481280
});
10491281
});

0 commit comments

Comments
 (0)