Skip to content

Commit 69037ec

Browse files
authored
Merge pull request #260856 from microsoft/tyriar/260781
Improve sub-command detection via common redirection
2 parents 4fda7c3 + b5a46fd commit 69037ec

File tree

2 files changed

+300
-12
lines changed

2 files changed

+300
-12
lines changed

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,45 @@
66
import type { OperatingSystem } from '../../../../../base/common/platform.js';
77
import { isPowerShell } from './runInTerminalHelpers.js';
88

9+
function createNumberRange(start: number, end: number): string[] {
10+
return Array.from({ length: end - start + 1 }, (_, i) => (start + i).toString());
11+
}
12+
13+
function sortByStringLengthDesc(arr: string[]): string[] {
14+
return [...arr].sort((a, b) => b.length - a.length);
15+
}
16+
917
// Derived from https://github.com/microsoft/vscode/blob/315b0949786b3807f05cb6acd13bf0029690a052/extensions/terminal-suggest/src/tokens.ts#L14-L18
10-
// Some of these can match the same string, so the order matters. Always put the more specific one
11-
// first (eg. >> before >)
18+
// Some of these can match the same string, so the order matters.
19+
//
20+
// This isn't perfect, at some point it would be better off moving over to tree sitter for this
21+
// instead of simple string matching.
1222
const shellTypeResetChars = new Map<'sh' | 'zsh' | 'pwsh', string[]>([
13-
['sh', ['&>>', '2>>', '>>', '2>', '&>', '||', '&&', '|&', '<<', '&', ';', '{', '>', '<', '|']],
14-
['zsh', ['<<<', '2>>', '&>>', '>>', '2>', '&>', '<(', '<>', '||', '&&', '|&', '&', ';', '{', '<<', '<(', '>', '<', '|']],
15-
['pwsh', ['*>>', '2>>', '>>', '2>', '&&', '*>', '>', '<', '|', ';', '!', '&']],
23+
['sh', sortByStringLengthDesc([
24+
// Redirection docs (bash) https://www.gnu.org/software/bash/manual/html_node/Redirections.html
25+
...createNumberRange(1, 9).concat('').map(n => `${n}<<<`), // Here strings
26+
...createNumberRange(1, 9).concat('').flatMap(n => createNumberRange(1, 9).map(m => `${n}>&${m}`)), // Redirect stream to stream
27+
...createNumberRange(1, 9).concat('').map(n => `${n}<>`), // Open file descriptor for reading and writing
28+
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>>`),
29+
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>`),
30+
'0<', '||', '&&', '|&', '<<', '&', ';', '{', '>', '<', '|'
31+
])],
32+
['zsh', sortByStringLengthDesc([
33+
// Redirection docs https://zsh.sourceforge.io/Doc/Release/Redirection.html
34+
...createNumberRange(1, 9).concat('').map(n => `${n}<<<`), // Here strings
35+
...createNumberRange(1, 9).concat('').flatMap(n => createNumberRange(1, 9).map(m => `${n}>&${m}`)), // Redirect stream to stream
36+
...createNumberRange(1, 9).concat('').map(n => `${n}<>`), // Open file descriptor for reading and writing
37+
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>>`),
38+
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>`),
39+
'<(', '||', '>|', '>!', '&&', '|&', '&', ';', '{', '<(', '<', '|'
40+
])],
41+
['pwsh', sortByStringLengthDesc([
42+
// Redirection docs: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_redirection?view=powershell-7.5
43+
...createNumberRange(1, 6).concat('*', '').flatMap(n => createNumberRange(1, 6).map(m => `${n}>&${m}`)), // Stream to stream redirection
44+
...createNumberRange(1, 6).concat('*', '').map(n => `${n}>>`),
45+
...createNumberRange(1, 6).concat('*', '').map(n => `${n}>`),
46+
'&&', '<', '|', ';', '!', '&'
47+
])],
1648
]);
1749

1850
export function splitCommandLineIntoSubCommands(commandLine: string, envShell: string, envOS: OperatingSystem): string[] {

src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts

Lines changed: 263 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,269 @@ suite('splitCommandLineIntoSubCommands', () => {
188188
});
189189
});
190190

191+
suite('redirection tests', () => {
192+
suite('output redirection', () => {
193+
test('should split on basic output redirection', () => {
194+
const commandLine = 'echo hello > output.txt';
195+
const expectedSubCommands = ['echo hello', 'output.txt'];
196+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
197+
deepStrictEqual(actualSubCommands, expectedSubCommands);
198+
});
199+
200+
test('should split on append redirection', () => {
201+
const commandLine = 'echo hello >> output.txt';
202+
const expectedSubCommands = ['echo hello', 'output.txt'];
203+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
204+
deepStrictEqual(actualSubCommands, expectedSubCommands);
205+
});
206+
207+
test('should split on multiple output redirections', () => {
208+
const commandLine = 'ls > files.txt && cat files.txt > backup.txt';
209+
const expectedSubCommands = ['ls', 'files.txt', 'cat files.txt', 'backup.txt'];
210+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
211+
deepStrictEqual(actualSubCommands, expectedSubCommands);
212+
});
213+
214+
test('should split on numbered file descriptor redirection', () => {
215+
const commandLine = 'command 1> stdout.txt 2> stderr.txt';
216+
const expectedSubCommands = ['command', 'stdout.txt', 'stderr.txt'];
217+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
218+
deepStrictEqual(actualSubCommands, expectedSubCommands);
219+
});
220+
221+
test('should split on stderr-only redirection', () => {
222+
const commandLine = 'make 2> errors.log';
223+
const expectedSubCommands = ['make', 'errors.log'];
224+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
225+
deepStrictEqual(actualSubCommands, expectedSubCommands);
226+
});
227+
228+
test('should split on all output redirection (&>)', () => {
229+
const commandLine = 'command &> all_output.txt';
230+
const expectedSubCommands = ['command', 'all_output.txt'];
231+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
232+
deepStrictEqual(actualSubCommands, expectedSubCommands);
233+
});
234+
});
235+
236+
suite('input redirection', () => {
237+
test('should split on input redirection', () => {
238+
const commandLine = 'sort < input.txt';
239+
const expectedSubCommands = ['sort', 'input.txt'];
240+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
241+
deepStrictEqual(actualSubCommands, expectedSubCommands);
242+
});
243+
244+
test('should split on numbered input redirection', () => {
245+
const commandLine = 'program 0< input.txt';
246+
const expectedSubCommands = ['program', 'input.txt'];
247+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
248+
deepStrictEqual(actualSubCommands, expectedSubCommands);
249+
});
250+
251+
test('should split on input/output combined', () => {
252+
const commandLine = 'sort < input.txt > output.txt';
253+
const expectedSubCommands = ['sort', 'input.txt', 'output.txt'];
254+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
255+
deepStrictEqual(actualSubCommands, expectedSubCommands);
256+
});
257+
});
258+
259+
suite('stream redirection', () => {
260+
test('should split on stdout to stderr redirection', () => {
261+
const commandLine = 'echo error 1>&2';
262+
const expectedSubCommands = ['echo error', ''];
263+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
264+
deepStrictEqual(actualSubCommands, expectedSubCommands);
265+
});
266+
267+
test('should split on stderr to stdout redirection', () => {
268+
const commandLine = 'command 2>&1';
269+
const expectedSubCommands = ['command', ''];
270+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
271+
deepStrictEqual(actualSubCommands, expectedSubCommands);
272+
});
273+
274+
test('should split on stream redirection with numbered descriptors', () => {
275+
const commandLine = 'exec 3>&1 && exec 4>&2';
276+
const expectedSubCommands = ['exec', '', 'exec', ''];
277+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
278+
deepStrictEqual(actualSubCommands, expectedSubCommands);
279+
});
280+
281+
test('should split on multiple stream redirections', () => {
282+
const commandLine = 'command 2>&1 1>&3 3>&2';
283+
const expectedSubCommands = ['command', '', '', ''];
284+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
285+
deepStrictEqual(actualSubCommands, expectedSubCommands);
286+
});
287+
});
288+
289+
suite('here documents and here strings', () => {
290+
test('should split on here document', () => {
291+
const commandLine = 'cat << EOF && echo done';
292+
const expectedSubCommands = ['cat', 'EOF', 'echo done'];
293+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
294+
deepStrictEqual(actualSubCommands, expectedSubCommands);
295+
});
296+
297+
test('should split on here string (bash/zsh)', () => {
298+
const commandLine = 'grep pattern <<< "search this text"';
299+
const expectedSubCommands = ['grep pattern', '"search this text"'];
300+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
301+
deepStrictEqual(actualSubCommands, expectedSubCommands);
302+
});
303+
304+
test('should split on numbered here string', () => {
305+
const commandLine = 'command 3<<< "input data"';
306+
const expectedSubCommands = ['command', '"input data"'];
307+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
308+
deepStrictEqual(actualSubCommands, expectedSubCommands);
309+
});
310+
});
311+
312+
suite('bidirectional redirection', () => {
313+
test('should split on read/write redirection', () => {
314+
const commandLine = 'dialog <> /dev/tty1';
315+
const expectedSubCommands = ['dialog', '/dev/tty1'];
316+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
317+
deepStrictEqual(actualSubCommands, expectedSubCommands);
318+
});
319+
320+
test('should split on numbered bidirectional redirection', () => {
321+
const commandLine = 'program 3<> data.file';
322+
const expectedSubCommands = ['program', 'data.file'];
323+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
324+
deepStrictEqual(actualSubCommands, expectedSubCommands);
325+
});
326+
});
327+
328+
suite('PowerShell redirection', () => {
329+
test('should split on PowerShell output redirection', () => {
330+
const commandLine = 'Get-Process > processes.txt';
331+
const expectedSubCommands = ['Get-Process', 'processes.txt'];
332+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
333+
deepStrictEqual(actualSubCommands, expectedSubCommands);
334+
});
335+
336+
test('should split on PowerShell append redirection', () => {
337+
const commandLine = 'Write-Output "log entry" >> log.txt';
338+
const expectedSubCommands = ['Write-Output "log entry"', 'log.txt'];
339+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
340+
deepStrictEqual(actualSubCommands, expectedSubCommands);
341+
});
342+
343+
test('should split on PowerShell error stream redirection', () => {
344+
const commandLine = 'Get-Content nonexistent.txt 2> errors.log';
345+
const expectedSubCommands = ['Get-Content nonexistent.txt', 'errors.log'];
346+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
347+
deepStrictEqual(actualSubCommands, expectedSubCommands);
348+
});
349+
350+
test('should split on PowerShell warning stream redirection', () => {
351+
const commandLine = 'Get-Process 3> warnings.log';
352+
const expectedSubCommands = ['Get-Process', 'warnings.log'];
353+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
354+
deepStrictEqual(actualSubCommands, expectedSubCommands);
355+
});
356+
357+
test('should split on PowerShell verbose stream redirection', () => {
358+
const commandLine = 'Get-ChildItem 4> verbose.log';
359+
const expectedSubCommands = ['Get-ChildItem', 'verbose.log'];
360+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
361+
deepStrictEqual(actualSubCommands, expectedSubCommands);
362+
});
363+
364+
test('should split on PowerShell debug stream redirection', () => {
365+
const commandLine = 'Invoke-Command 5> debug.log';
366+
const expectedSubCommands = ['Invoke-Command', 'debug.log'];
367+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
368+
deepStrictEqual(actualSubCommands, expectedSubCommands);
369+
});
370+
371+
test('should split on PowerShell information stream redirection', () => {
372+
const commandLine = 'Write-Information "info" 6> info.log';
373+
const expectedSubCommands = ['Write-Information "info"', 'info.log'];
374+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
375+
deepStrictEqual(actualSubCommands, expectedSubCommands);
376+
});
377+
378+
test('should split on PowerShell all streams redirection', () => {
379+
const commandLine = 'Get-Process *> all_streams.log';
380+
const expectedSubCommands = ['Get-Process', 'all_streams.log'];
381+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
382+
deepStrictEqual(actualSubCommands, expectedSubCommands);
383+
});
384+
385+
test('should split on PowerShell stream to stream redirection', () => {
386+
const commandLine = 'Write-Error "error" 2>&1';
387+
const expectedSubCommands = ['Write-Error "error"', ''];
388+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
389+
deepStrictEqual(actualSubCommands, expectedSubCommands);
390+
});
391+
});
392+
393+
suite('complex redirection scenarios', () => {
394+
test('should split on command with multiple redirections', () => {
395+
const commandLine = 'command < input.txt > output.txt 2> errors.log';
396+
const expectedSubCommands = ['command', 'input.txt', 'output.txt', 'errors.log'];
397+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
398+
deepStrictEqual(actualSubCommands, expectedSubCommands);
399+
});
400+
401+
test('should split on redirection with pipes and logical operators', () => {
402+
const commandLine = 'cat file.txt | grep pattern > results.txt && echo "Found" || echo "Not found" 2> errors.log';
403+
const expectedSubCommands = ['cat file.txt', 'grep pattern', 'results.txt', 'echo "Found"', 'echo "Not found"', 'errors.log'];
404+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
405+
deepStrictEqual(actualSubCommands, expectedSubCommands);
406+
});
407+
408+
test('should split on chained redirections', () => {
409+
const commandLine = 'echo "step1" > temp.txt && cat temp.txt >> final.txt && rm temp.txt';
410+
const expectedSubCommands = ['echo "step1"', 'temp.txt', 'cat temp.txt', 'final.txt', 'rm temp.txt'];
411+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
412+
deepStrictEqual(actualSubCommands, expectedSubCommands);
413+
});
414+
415+
test('should handle redirection with background processes', () => {
416+
const commandLine = 'long_running_command > output.log 2>&1 & echo "started"';
417+
const expectedSubCommands = ['long_running_command', 'output.log', '', 'echo "started"'];
418+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
419+
deepStrictEqual(actualSubCommands, expectedSubCommands);
420+
});
421+
});
422+
423+
suite('zsh-specific redirection', () => {
424+
test('should split on zsh noclobber override', () => {
425+
const commandLine = 'echo "force" >! existing_file.txt';
426+
const expectedSubCommands = ['echo "force"', 'existing_file.txt'];
427+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
428+
deepStrictEqual(actualSubCommands, expectedSubCommands);
429+
});
430+
431+
test('should split on zsh clobber override', () => {
432+
const commandLine = 'echo "overwrite" >| protected_file.txt';
433+
const expectedSubCommands = ['echo "overwrite"', 'protected_file.txt'];
434+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
435+
deepStrictEqual(actualSubCommands, expectedSubCommands);
436+
});
437+
438+
test('should split on zsh process substitution for input', () => {
439+
const commandLine = 'diff <(sort file1.txt) <(sort file2.txt)';
440+
const expectedSubCommands = ['diff', 'sort file1.txt)', 'sort file2.txt)'];
441+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
442+
deepStrictEqual(actualSubCommands, expectedSubCommands);
443+
});
444+
445+
test('should split on zsh multios', () => {
446+
const commandLine = 'echo "test" | tee >(gzip > file1.gz) >(bzip2 > file1.bz2)';
447+
const expectedSubCommands = ['echo "test"', 'tee', '(gzip', 'file1.gz)', '(bzip2', 'file1.bz2)'];
448+
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
449+
deepStrictEqual(actualSubCommands, expectedSubCommands);
450+
});
451+
});
452+
});
453+
191454
suite('complex command combinations', () => {
192455
test('should handle mixed operators in order', () => {
193456
const commandLine = 'ls | grep test && echo found > result.txt || echo failed';
@@ -202,13 +465,6 @@ suite('splitCommandLineIntoSubCommands', () => {
202465
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
203466
deepStrictEqual(actualSubCommands, expectedSubCommands);
204467
});
205-
206-
test('should handle here documents', () => {
207-
const commandLine = 'cat << EOF && echo done';
208-
const expectedSubCommands = ['cat', 'EOF', 'echo done'];
209-
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
210-
deepStrictEqual(actualSubCommands, expectedSubCommands);
211-
});
212468
});
213469
});
214470

0 commit comments

Comments
 (0)