Skip to content

Commit 124b1e9

Browse files
edvilmeCopilot
andcommitted
Use shell-quote for POSIX shells
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e3ff30d commit 124b1e9

2 files changed

Lines changed: 62 additions & 45 deletions

File tree

‎src/features/terminal/runInTerminal.ts‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { traceLog } from '../../common/logging';
1111
import { SHELL_INTEGRATION_TIMEOUT, waitForShellIntegration } from './utils';
1212

1313
const shellIntegrationWaits = new WeakMap<Terminal, Promise<boolean>>();
14+
const posixShells = new Set([
15+
ShellConstants.SH,
16+
ShellConstants.BASH,
17+
ShellConstants.ZSH,
18+
ShellConstants.KSH,
19+
ShellConstants.GITBASH,
20+
]);
1421

1522
async function waitForShellIntegrationBeforeRun(terminal: Terminal): Promise<void> {
1623
let waitPromise = shellIntegrationWaits.get(terminal);
@@ -21,8 +28,8 @@ async function waitForShellIntegrationBeforeRun(terminal: Terminal): Promise<voi
2128
await waitPromise;
2229
}
2330

24-
function getZshCommand(executable: string, args: string[]): string {
25-
return quote([executable, ...args]);
31+
function getPosixShellCommand(shellType: string, executable: string, args: string[]): string | undefined {
32+
return posixShells.has(shellType) ? quote([executable, ...args]) : undefined;
2633
}
2734

2835
export async function runInTerminal(
@@ -46,7 +53,7 @@ export async function runInTerminal(
4653
if (!terminal.shellIntegration) {
4754
await waitForShellIntegrationBeforeRun(terminal);
4855
}
49-
const zshCommand = shellType === ShellConstants.ZSH ? getZshCommand(executable, allArgs) : undefined;
56+
const posixShellCommand = getPosixShellCommand(shellType, executable, allArgs);
5057
if (terminal.shellIntegration) {
5158
let execution: TerminalShellExecution | undefined;
5259
const deferred = createDeferred<void>();
@@ -57,9 +64,9 @@ export async function runInTerminal(
5764
}
5865
});
5966

60-
if (zshCommand) {
61-
execution = terminal.shellIntegration.executeCommand(zshCommand);
62-
traceLog(`runInTerminal: executeCommand ${zshCommand}`);
67+
if (posixShellCommand !== undefined) {
68+
execution = terminal.shellIntegration.executeCommand(posixShellCommand);
69+
traceLog(`runInTerminal: executeCommand ${posixShellCommand}`);
6370
} else {
6471
const shouldSurroundWithQuotes =
6572
executable.includes(' ') && !executable.startsWith('"') && !executable.endsWith('"');
@@ -77,7 +84,7 @@ export async function runInTerminal(
7784
}
7885
await deferred.promise;
7986
} else {
80-
let text = zshCommand ?? quoteArgs([executable, ...allArgs]).join(' ');
87+
let text = posixShellCommand ?? quoteArgs([executable, ...allArgs]).join(' ');
8188
if (shellType === ShellConstants.PWSH && !text.startsWith('&')) {
8289
// PowerShell requires commands to be prefixed with '&' to run them.
8390
text = `& ${text}`;

‎src/test/features/terminal/runInTerminal.unit.test.ts‎

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@ import * as assert from 'assert';
22
import * as sinon from 'sinon';
33
import { Disposable, Terminal, TerminalShellExecution, TerminalShellExecutionEndEvent } from 'vscode';
44
import * as windowApis from '../../../common/window.apis';
5+
import { ShellConstants } from '../../../features/common/shellConstants';
56
import * as shellDetector from '../../../features/common/shellDetector';
67
import { runInTerminal } from '../../../features/terminal/runInTerminal';
78
import * as terminalUtils from '../../../features/terminal/utils';
89
import { createMockPythonEnvironment } from '../../mocks/pythonEnvironment';
910

1011
suite('runInTerminal', () => {
12+
const posixShells = [
13+
ShellConstants.SH,
14+
ShellConstants.BASH,
15+
ShellConstants.ZSH,
16+
ShellConstants.KSH,
17+
ShellConstants.GITBASH,
18+
];
19+
1120
teardown(() => {
1221
sinon.restore();
1322
});
@@ -50,7 +59,7 @@ suite('runInTerminal', () => {
5059
await new Promise<void>((resolve) => setImmediate(resolve));
5160

5261
sinon.assert.notCalled(terminal.sendText as sinon.SinonStub);
53-
sinon.assert.calledOnce(shellIntegration.executeCommand);
62+
sinon.assert.calledOnceWithExactly(shellIntegration.executeCommand, 'python', ['main.py']);
5463
assert.ok(endListener, 'shell execution end listener should be registered');
5564

5665
endListener!({ terminal, execution } as unknown as TerminalShellExecutionEndEvent);
@@ -74,8 +83,8 @@ suite('runInTerminal', () => {
7483
sinon.assert.calledOnceWithExactly(sendText, 'python main.py\n');
7584
});
7685

77-
test('preserves special characters in zsh script paths with shell integration', async () => {
78-
sinon.stub(shellDetector, 'identifyTerminalShell').returns('zsh');
86+
test('preserves special characters in POSIX shell arguments with shell integration', async () => {
87+
const identifyTerminalShellStub = sinon.stub(shellDetector, 'identifyTerminalShell');
7988
const execution = {} as TerminalShellExecution;
8089
const executeCommand = sinon.stub().returns(execution);
8190
const terminal = {
@@ -98,48 +107,49 @@ suite('runInTerminal', () => {
98107
['/workspace/$(touch marker).py', "python '/workspace/$(touch marker).py'"],
99108
];
100109

101-
for (const [script, command] of cases) {
102-
const runPromise = runInTerminal(environment, terminal, { cwd: '/workspace', args: [script] });
103-
await new Promise<void>((resolve) => setImmediate(resolve));
104-
sinon.assert.calledWithExactly(executeCommand, command);
105-
assert.ok(endListener, 'shell execution end listener should be registered');
106-
endListener!({ terminal, execution } as unknown as TerminalShellExecutionEndEvent);
107-
await runPromise;
110+
let callIndex = 0;
111+
for (const shell of posixShells) {
112+
identifyTerminalShellStub.returns(shell);
113+
for (const [script, command] of cases) {
114+
const runPromise = runInTerminal(environment, terminal, { cwd: '/workspace', args: [script] });
115+
await new Promise<void>((resolve) => setImmediate(resolve));
116+
assert.deepStrictEqual(executeCommand.getCall(callIndex).args, [command]);
117+
assert.ok(endListener, 'shell execution end listener should be registered');
118+
endListener!({ terminal, execution } as unknown as TerminalShellExecutionEndEvent);
119+
await runPromise;
120+
callIndex += 1;
121+
}
108122
}
123+
sinon.assert.callCount(executeCommand, posixShells.length * cases.length);
109124
});
110125

111-
test('preserves special characters in zsh script paths without shell integration', async () => {
112-
sinon.stub(shellDetector, 'identifyTerminalShell').returns('zsh');
126+
test('preserves special characters in POSIX shell arguments without shell integration', async () => {
127+
const identifyTerminalShellStub = sinon.stub(shellDetector, 'identifyTerminalShell');
113128
sinon.stub(terminalUtils, 'waitForShellIntegration').resolves(false);
114129
const sendText = sinon.stub();
115-
const terminal = {
116-
name: 'Python',
117-
shellIntegration: undefined,
118-
sendText,
119-
} as unknown as Terminal;
120130
const environment = createMockPythonEnvironment({ envPath: '/env/bin/python' });
131+
const cases = [
132+
['/workspace/cost$AUDIT_UNSET.py', 'python /workspace/cost\\$AUDIT_UNSET.py\n'],
133+
["/workspace/it's_python.py", 'python "/workspace/it\'s_python.py"\n'],
134+
['=report.py', 'python \\=report.py\n'],
135+
['/workspace/$(touch marker).py', "python '/workspace/$(touch marker).py'\n"],
136+
];
121137

122-
await runInTerminal(environment, terminal, {
123-
cwd: '/workspace',
124-
args: ['/workspace/cost$AUDIT_UNSET.py'],
125-
});
126-
await runInTerminal(environment, terminal, {
127-
cwd: '/workspace',
128-
args: ["/workspace/it's_python.py"],
129-
});
130-
await runInTerminal(environment, terminal, {
131-
cwd: '/workspace',
132-
args: ['=report.py'],
133-
});
134-
await runInTerminal(environment, terminal, {
135-
cwd: '/workspace',
136-
args: ['/workspace/$(touch marker).py'],
137-
});
138-
139-
sinon.assert.calledWithExactly(sendText, 'python /workspace/cost\\$AUDIT_UNSET.py\n');
140-
sinon.assert.calledWithExactly(sendText, 'python "/workspace/it\'s_python.py"\n');
141-
sinon.assert.calledWithExactly(sendText, 'python \\=report.py\n');
142-
sinon.assert.calledWithExactly(sendText, "python '/workspace/$(touch marker).py'\n");
138+
let callIndex = 0;
139+
for (const shell of posixShells) {
140+
identifyTerminalShellStub.returns(shell);
141+
const terminal = {
142+
name: 'Python',
143+
shellIntegration: undefined,
144+
sendText,
145+
} as unknown as Terminal;
146+
for (const [script, command] of cases) {
147+
await runInTerminal(environment, terminal, { cwd: '/workspace', args: [script] });
148+
assert.deepStrictEqual(sendText.getCall(callIndex).args, [command]);
149+
callIndex += 1;
150+
}
151+
}
152+
sinon.assert.callCount(sendText, posixShells.length * cases.length);
143153
});
144154

145155
test('does not wait again when reusing a terminal without shell integration', async () => {

0 commit comments

Comments
 (0)