diff --git a/eslint.config.js b/eslint.config.js index 509648a12c1..9fb6db94c50 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1824,6 +1824,7 @@ export default tseslint.config( // terminalContrib is one extra folder deep 'vs/workbench/contrib/terminalContrib/*/~', 'vscode-notebook-renderer', // Type only import + '@vscode/tree-sitter-wasm', // type import { 'when': 'hasBrowser', 'pattern': '@xterm/xterm' diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index 73a83f5cec4..291d53028c9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -11,7 +11,6 @@ import { structuralEquals } from '../../../../../base/common/equals.js'; import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; import { isPowerShell } from './runInTerminalHelpers.js'; -import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; export interface IAutoApproveRule { regex: RegExp; @@ -40,7 +39,6 @@ export class CommandLineAutoApprover extends Disposable { constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService ) { super(); this.updateConfiguration(); @@ -52,38 +50,6 @@ export class CommandLineAutoApprover extends Disposable { this.updateConfiguration(); } })); - - const parserClass = this._treeSitterLibraryService.getParserClass(); - parserClass.then(async parserCtor => { - const bashLang = await this._treeSitterLibraryService.getLanguage('bash'); - const pwshLang = await this._treeSitterLibraryService.getLanguage('powershell'); - - const parser = new parserCtor(); - if (bashLang) { - parser.setLanguage(bashLang); - const tree = parser.parse('echo "$(evil) a|b|c" | ls'); - - const q = await this._treeSitterLibraryService.createQuery('bash', '(command) @command'); - if (tree && q) { - const captures = q.captures(tree.rootNode); - const subCommands = captures.map(e => e.node.text); - console.log('done', subCommands); - } - } - - if (pwshLang) { - parser.setLanguage(pwshLang); - const tree = parser.parse('Get-ChildItem | Write-Host "$(evil)"'); - - const q = await this._treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)'); - if (tree && q) { - const captures = q.captures(tree.rootNode); - const subCommands = captures.map(e => e.node.text); - console.log('done', subCommands); - } - } - }); - } updateConfiguration() { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts deleted file mode 100644 index df98f6c64f8..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts +++ /dev/null @@ -1,75 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { OperatingSystem } from '../../../../../base/common/platform.js'; -import { isPowerShell } from './runInTerminalHelpers.js'; - -function createNumberRange(start: number, end: number): string[] { - return Array.from({ length: end - start + 1 }, (_, i) => (start + i).toString()); -} - -function sortByStringLengthDesc(arr: string[]): string[] { - return [...arr].sort((a, b) => b.length - a.length); -} - -// Derived from https://github.com/microsoft/vscode/blob/315b0949786b3807f05cb6acd13bf0029690a052/extensions/terminal-suggest/src/tokens.ts#L14-L18 -// Some of these can match the same string, so the order matters. -// -// This isn't perfect, at some point it would be better off moving over to tree sitter for this -// instead of simple string matching. -const shellTypeResetChars = new Map<'sh' | 'zsh' | 'pwsh', string[]>([ - ['sh', sortByStringLengthDesc([ - // Redirection docs (bash) https://www.gnu.org/software/bash/manual/html_node/Redirections.html - ...createNumberRange(1, 9).concat('').map(n => `${n}<<<`), // Here strings - ...createNumberRange(1, 9).concat('').flatMap(n => createNumberRange(1, 9).map(m => `${n}>&${m}`)), // Redirect stream to stream - ...createNumberRange(1, 9).concat('').map(n => `${n}<>`), // Open file descriptor for reading and writing - ...createNumberRange(1, 9).concat('&', '').map(n => `${n}>>`), - ...createNumberRange(1, 9).concat('&', '').map(n => `${n}>`), - '0<', '||', '&&', '|&', '<<', '&', ';', '{', '>', '<', '|' - ])], - ['zsh', sortByStringLengthDesc([ - // Redirection docs https://zsh.sourceforge.io/Doc/Release/Redirection.html - ...createNumberRange(1, 9).concat('').map(n => `${n}<<<`), // Here strings - ...createNumberRange(1, 9).concat('').flatMap(n => createNumberRange(1, 9).map(m => `${n}>&${m}`)), // Redirect stream to stream - ...createNumberRange(1, 9).concat('').map(n => `${n}<>`), // Open file descriptor for reading and writing - ...createNumberRange(1, 9).concat('&', '').map(n => `${n}>>`), - ...createNumberRange(1, 9).concat('&', '').map(n => `${n}>`), - '<(', '||', '>|', '>!', '&&', '|&', '&', ';', '{', '<(', '<', '|' - ])], - ['pwsh', sortByStringLengthDesc([ - // Redirection docs: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_redirection?view=powershell-7.5 - ...createNumberRange(1, 6).concat('*', '').flatMap(n => createNumberRange(1, 6).map(m => `${n}>&${m}`)), // Stream to stream redirection - ...createNumberRange(1, 6).concat('*', '').map(n => `${n}>>`), - ...createNumberRange(1, 6).concat('*', '').map(n => `${n}>`), - '&&', '<', '|', ';', '!', '&' - ])], -]); - -export function splitCommandLineIntoSubCommands(commandLine: string, envShell: string, envOS: OperatingSystem): string[] { - let shellType: 'sh' | 'zsh' | 'pwsh'; - const envShellWithoutExe = envShell.replace(/\.exe$/, ''); - if (isPowerShell(envShell, envOS)) { - shellType = 'pwsh'; - } else { - switch (envShellWithoutExe) { - case 'zsh': shellType = 'zsh'; break; - default: shellType = 'sh'; break; - } - } - const subCommands = [commandLine]; - const resetChars = shellTypeResetChars.get(shellType); - if (resetChars) { - for (const chars of resetChars) { - for (let i = 0; i < subCommands.length; i++) { - const subCommand = subCommands[i]; - if (subCommand.includes(chars)) { - subCommands.splice(i, 1, ...subCommand.split(chars).map(e => e.trim())); - i--; - } - } - } - } - return subCommands.filter(e => e.length > 0); -} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index ab6bec13e81..59d79407f44 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -43,10 +43,10 @@ import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; import { dedupeRules, generateAutoApproveActions, isPowerShell } from '../runInTerminalHelpers.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; -import { splitCommandLineIntoSubCommands } from '../subCommands.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { OutputMonitor } from './monitoring/outputMonitor.js'; import { IPollingResult, OutputMonitorState } from './monitoring/types.js'; +import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; const enum TerminalToolStorageKeysInternal { TerminalSession = 'chat.terminalSessions' @@ -148,6 +148,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { private readonly _terminalToolCreator: ToolTerminalCreator; private readonly _commandSimplifier: CommandSimplifier; + private readonly _treeSitterCommandParser: TreeSitterCommandParser; private readonly _telemetry: RunInTerminalToolTelemetry; protected readonly _commandLineAutoApprover: CommandLineAutoApprover; protected readonly _sessionTerminalAssociations: Map = new Map(); @@ -182,6 +183,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._terminalToolCreator = _instantiationService.createInstance(ToolTerminalCreator); this._commandSimplifier = _instantiationService.createInstance(CommandSimplifier, this._osBackend); + this._treeSitterCommandParser = this._instantiationService.createInstance(TreeSitterCommandParser); this._telemetry = _instantiationService.createInstance(RunInTerminalToolTelemetry); this._commandLineAutoApprover = this._register(_instantiationService.createInstance(CommandLineAutoApprover)); @@ -237,7 +239,14 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { // can be reviewed in the terminal channel. It also allows gauging the effective set of // commands that would be auto approved if it were enabled. const actualCommand = toolEditedCommand ?? args.command; - const subCommands = splitCommandLineIntoSubCommands(actualCommand, shell, os); + + const treeSitterLanguage = isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash; + // TODO: Handle throw + const subCommands = await this._treeSitterCommandParser.extractSubCommands(treeSitterLanguage, actualCommand); + this._logService.info('RunInTerminalTool: autoApprove: Parsed sub-commands', subCommands); + + + // const subCommands = splitCommandLineIntoSubCommands(actualCommand, shell, os); const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os)); const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(actualCommand); const autoApproveReasons: string[] = [ @@ -252,30 +261,30 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const deniedSubCommandResult = subCommandResults.find(e => e.result === 'denied'); if (deniedSubCommandResult) { - this._logService.info('autoApprove: Sub-command DENIED auto approval'); + this._logService.info('RunInTerminalTool: autoApprove: Sub-command DENIED auto approval'); isDenied = true; autoApproveDefault = deniedSubCommandResult.rule?.isDefaultRule; autoApproveReason = 'subCommand'; } else if (commandLineResult.result === 'denied') { - this._logService.info('autoApprove: Command line DENIED auto approval'); + this._logService.info('RunInTerminalTool: autoApprove: Command line DENIED auto approval'); isDenied = true; autoApproveDefault = commandLineResult.rule?.isDefaultRule; autoApproveReason = 'commandLine'; } else { if (subCommandResults.every(e => e.result === 'approved')) { - this._logService.info('autoApprove: All sub-commands auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: All sub-commands auto-approved'); autoApproveReason = 'subCommand'; isAutoApproved = true; autoApproveDefault = subCommandResults.every(e => e.rule?.isDefaultRule); } else { - this._logService.info('autoApprove: All sub-commands NOT auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: All sub-commands NOT auto-approved'); if (commandLineResult.result === 'approved') { - this._logService.info('autoApprove: Command line auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: Command line auto-approved'); autoApproveReason = 'commandLine'; isAutoApproved = true; autoApproveDefault = commandLineResult.rule?.isDefaultRule; } else { - this._logService.info('autoApprove: Command line NOT auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: Command line NOT auto-approved'); } } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts new file mode 100644 index 00000000000..b495aaa51e6 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BugIndicatingError } from '../../../../../base/common/errors.js'; +import { Lazy } from '../../../../../base/common/lazy.js'; +import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; +import type { Parser, Query } from '@vscode/tree-sitter-wasm'; + +export const enum TreeSitterCommandParserLanguage { + Bash = 'bash', + PowerShell = 'powershell', +} + +export class TreeSitterCommandParser { + private readonly _parser: Promise; + + private readonly _languageToQueryMap = { + [TreeSitterCommandParserLanguage.Bash]: new Lazy(() => { + return this._treeSitterLibraryService.createQuery(TreeSitterCommandParserLanguage.Bash, '(command) @command'); + }), + [TreeSitterCommandParserLanguage.PowerShell]: new Lazy(() => { + return this._treeSitterLibraryService.createQuery(TreeSitterCommandParserLanguage.PowerShell, '(command\ncommand_name: (command_name) @function)'); + }), + } satisfies { [K in TreeSitterCommandParserLanguage]: Lazy> }; + + constructor( + @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService + ) { + this._parser = this._treeSitterLibraryService.getParserClass().then(ParserCtor => new ParserCtor()); + } + + async extractSubCommands(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { + const parser = await this._parser; + parser.setLanguage(await this._treeSitterLibraryService.getLanguage(languageId)); + + const tree = parser.parse(commandLine); + if (!tree) { + throw new BugIndicatingError('Failed to parse tree'); + } + + const query = await this._languageToQueryMap[languageId].value; + if (!query) { + throw new BugIndicatingError('Failed to create tree sitter query'); + } + + const captures = query.captures(tree.rootNode); + const subCommands = captures.map(e => e.node.text); + return subCommands; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts deleted file mode 100644 index cf1d81fd1ca..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts +++ /dev/null @@ -1,469 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { deepStrictEqual } from 'assert'; -import { splitCommandLineIntoSubCommands } from '../../browser/subCommands.js'; -import { OperatingSystem } from '../../../../../../base/common/platform.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; - -suite('splitCommandLineIntoSubCommands', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('should split command line into subcommands', () => { - const commandLine = 'echo "Hello World" && ls -la || pwd'; - const expectedSubCommands = ['echo "Hello World"', 'ls -la', 'pwd']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - suite('bash/sh shell', () => { - test('should split on logical operators', () => { - const commandLine = 'echo test && ls -la || pwd'; - const expectedSubCommands = ['echo test', 'ls -la', 'pwd']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on pipes', () => { - const commandLine = 'ls -la | grep test | wc -l'; - const expectedSubCommands = ['ls -la', 'grep test', 'wc -l']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on semicolons', () => { - const commandLine = 'cd /tmp; ls -la; pwd'; - const expectedSubCommands = ['cd /tmp', 'ls -la', 'pwd']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on background operator', () => { - const commandLine = 'sleep 5 & echo done'; - const expectedSubCommands = ['sleep 5', 'echo done']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on redirection operators', () => { - const commandLine = 'echo test > output.txt && cat output.txt'; - const expectedSubCommands = ['echo test', 'output.txt', 'cat output.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on stderr redirection', () => { - const commandLine = 'command 2> error.log && echo success'; - const expectedSubCommands = ['command', 'error.log', 'echo success']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on append redirection', () => { - const commandLine = 'echo line1 >> file.txt && echo line2 >> file.txt'; - const expectedSubCommands = ['echo line1', 'file.txt', 'echo line2', 'file.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('zsh shell', () => { - test('should split on zsh-specific operators', () => { - const commandLine = 'echo test <<< "input" && ls'; - const expectedSubCommands = ['echo test', '"input"', 'ls']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on process substitution', () => { - const commandLine = 'diff <(ls dir1) <(ls dir2)'; - const expectedSubCommands = ['diff', 'ls dir1)', 'ls dir2)']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on bidirectional redirection', () => { - const commandLine = 'command <> file.txt && echo done'; - const expectedSubCommands = ['command', 'file.txt', 'echo done']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should handle complex zsh command chains', () => { - const commandLine = 'ls | grep test && echo found || echo not found'; - const expectedSubCommands = ['ls', 'grep test', 'echo found', 'echo not found']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('PowerShell', () => { - test('should not split on PowerShell logical operators', () => { - const commandLine = 'Get-ChildItem -and Get-Location -or Write-Host "test"'; - const expectedSubCommands = ['Get-ChildItem -and Get-Location -or Write-Host "test"']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell pipes', () => { - const commandLine = 'Get-Process | Where-Object Name -eq "notepad" | Stop-Process'; - const expectedSubCommands = ['Get-Process', 'Where-Object Name -eq "notepad"', 'Stop-Process']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell redirection', () => { - const commandLine = 'Get-Process > processes.txt && Get-Content processes.txt'; - const expectedSubCommands = ['Get-Process', 'processes.txt', 'Get-Content processes.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('edge cases', () => { - test('should return single command when no operators present', () => { - const commandLine = 'echo "hello world"'; - const expectedSubCommands = ['echo "hello world"']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should handle empty command', () => { - const commandLine = ''; - const expectedSubCommands: string[] = []; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should trim whitespace from subcommands', () => { - const commandLine = 'echo test && ls -la || pwd'; - const expectedSubCommands = ['echo test', 'ls -la', 'pwd']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should handle multiple consecutive operators', () => { - const commandLine = 'echo test && && ls'; - const expectedSubCommands = ['echo test', 'ls']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should handle unknown shell as sh', () => { - const commandLine = 'echo test && ls -la'; - const expectedSubCommands = ['echo test', 'ls -la']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'unknown-shell', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('shell type detection', () => { - test('should detect PowerShell variants', () => { - const commandLine = 'Get-Process ; Get-Location'; - const expectedSubCommands = ['Get-Process', 'Get-Location']; - - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'powershell', OperatingSystem.Linux), expectedSubCommands); - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows), expectedSubCommands); - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'pwsh', OperatingSystem.Linux), expectedSubCommands); - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows), expectedSubCommands); - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'powershell-preview', OperatingSystem.Linux), expectedSubCommands); - }); - - test('should detect zsh specifically', () => { - const commandLine = 'echo test <<< input'; - const expectedSubCommands = ['echo test', 'input']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should default to sh for other shells', () => { - const commandLine = 'echo test && ls'; - const expectedSubCommands = ['echo test', 'ls']; - - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux), expectedSubCommands); - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'dash', OperatingSystem.Linux), expectedSubCommands); - deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'fish', OperatingSystem.Linux), expectedSubCommands); - }); - }); - - suite('redirection tests', () => { - suite('output redirection', () => { - test('should split on basic output redirection', () => { - const commandLine = 'echo hello > output.txt'; - const expectedSubCommands = ['echo hello', 'output.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on append redirection', () => { - const commandLine = 'echo hello >> output.txt'; - const expectedSubCommands = ['echo hello', 'output.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on multiple output redirections', () => { - const commandLine = 'ls > files.txt && cat files.txt > backup.txt'; - const expectedSubCommands = ['ls', 'files.txt', 'cat files.txt', 'backup.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on numbered file descriptor redirection', () => { - const commandLine = 'command 1> stdout.txt 2> stderr.txt'; - const expectedSubCommands = ['command', 'stdout.txt', 'stderr.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on stderr-only redirection', () => { - const commandLine = 'make 2> errors.log'; - const expectedSubCommands = ['make', 'errors.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on all output redirection (&>)', () => { - const commandLine = 'command &> all_output.txt'; - const expectedSubCommands = ['command', 'all_output.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('input redirection', () => { - test('should split on input redirection', () => { - const commandLine = 'sort < input.txt'; - const expectedSubCommands = ['sort', 'input.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on numbered input redirection', () => { - const commandLine = 'program 0< input.txt'; - const expectedSubCommands = ['program', 'input.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on input/output combined', () => { - const commandLine = 'sort < input.txt > output.txt'; - const expectedSubCommands = ['sort', 'input.txt', 'output.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('stream redirection', () => { - test('should split on stdout to stderr redirection', () => { - const commandLine = 'echo error 1>&2'; - const expectedSubCommands = ['echo error']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on stderr to stdout redirection', () => { - const commandLine = 'command 2>&1'; - const expectedSubCommands = ['command']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on stream redirection with numbered descriptors', () => { - const commandLine = 'exec 3>&1 && exec 4>&2'; - const expectedSubCommands = ['exec', 'exec']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on multiple stream redirections', () => { - const commandLine = 'command 2>&1 1>&3 3>&2'; - const expectedSubCommands = ['command']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('here documents and here strings', () => { - test('should split on here document', () => { - const commandLine = 'cat << EOF && echo done'; - const expectedSubCommands = ['cat', 'EOF', 'echo done']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on here string (bash/zsh)', () => { - const commandLine = 'grep pattern <<< "search this text"'; - const expectedSubCommands = ['grep pattern', '"search this text"']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on numbered here string', () => { - const commandLine = 'command 3<<< "input data"'; - const expectedSubCommands = ['command', '"input data"']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('bidirectional redirection', () => { - test('should split on read/write redirection', () => { - const commandLine = 'dialog <> /dev/tty1'; - const expectedSubCommands = ['dialog', '/dev/tty1']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on numbered bidirectional redirection', () => { - const commandLine = 'program 3<> data.file'; - const expectedSubCommands = ['program', 'data.file']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('PowerShell redirection', () => { - test('should split on PowerShell output redirection', () => { - const commandLine = 'Get-Process > processes.txt'; - const expectedSubCommands = ['Get-Process', 'processes.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell append redirection', () => { - const commandLine = 'Write-Output "log entry" >> log.txt'; - const expectedSubCommands = ['Write-Output "log entry"', 'log.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell error stream redirection', () => { - const commandLine = 'Get-Content nonexistent.txt 2> errors.log'; - const expectedSubCommands = ['Get-Content nonexistent.txt', 'errors.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell warning stream redirection', () => { - const commandLine = 'Get-Process 3> warnings.log'; - const expectedSubCommands = ['Get-Process', 'warnings.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell verbose stream redirection', () => { - const commandLine = 'Get-ChildItem 4> verbose.log'; - const expectedSubCommands = ['Get-ChildItem', 'verbose.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell debug stream redirection', () => { - const commandLine = 'Invoke-Command 5> debug.log'; - const expectedSubCommands = ['Invoke-Command', 'debug.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell information stream redirection', () => { - const commandLine = 'Write-Information "info" 6> info.log'; - const expectedSubCommands = ['Write-Information "info"', 'info.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell all streams redirection', () => { - const commandLine = 'Get-Process *> all_streams.log'; - const expectedSubCommands = ['Get-Process', 'all_streams.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on PowerShell stream to stream redirection', () => { - const commandLine = 'Write-Error "error" 2>&1'; - const expectedSubCommands = ['Write-Error "error"']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('complex redirection scenarios', () => { - test('should split on command with multiple redirections', () => { - const commandLine = 'command < input.txt > output.txt 2> errors.log'; - const expectedSubCommands = ['command', 'input.txt', 'output.txt', 'errors.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on redirection with pipes and logical operators', () => { - const commandLine = 'cat file.txt | grep pattern > results.txt && echo "Found" || echo "Not found" 2> errors.log'; - const expectedSubCommands = ['cat file.txt', 'grep pattern', 'results.txt', 'echo "Found"', 'echo "Not found"', 'errors.log']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on chained redirections', () => { - const commandLine = 'echo "step1" > temp.txt && cat temp.txt >> final.txt && rm temp.txt'; - const expectedSubCommands = ['echo "step1"', 'temp.txt', 'cat temp.txt', 'final.txt', 'rm temp.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should handle redirection with background processes', () => { - const commandLine = 'long_running_command > output.log 2>&1 & echo "started"'; - const expectedSubCommands = ['long_running_command', 'output.log', 'echo "started"']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - - suite('zsh-specific redirection', () => { - test('should split on zsh noclobber override', () => { - const commandLine = 'echo "force" >! existing_file.txt'; - const expectedSubCommands = ['echo "force"', 'existing_file.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on zsh clobber override', () => { - const commandLine = 'echo "overwrite" >| protected_file.txt'; - const expectedSubCommands = ['echo "overwrite"', 'protected_file.txt']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on zsh process substitution for input', () => { - const commandLine = 'diff <(sort file1.txt) <(sort file2.txt)'; - const expectedSubCommands = ['diff', 'sort file1.txt)', 'sort file2.txt)']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test('should split on zsh multios', () => { - const commandLine = 'echo "test" | tee >(gzip > file1.gz) >(bzip2 > file1.bz2)'; - const expectedSubCommands = ['echo "test"', 'tee', '(gzip', 'file1.gz)', '(bzip2', 'file1.bz2)']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); - }); - - suite('complex command combinations', () => { - test('should handle mixed operators in order', () => { - const commandLine = 'ls | grep test && echo found > result.txt || echo failed'; - const expectedSubCommands = ['ls', 'grep test', 'echo found', 'result.txt', 'echo failed']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - - test.skip('should handle subshells and braces', () => { - const commandLine = '(cd /tmp && ls) && { echo done; }'; - const expectedSubCommands = ['(cd /tmp', 'ls)', '{ echo done', '}']; - const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux); - deepStrictEqual(actualSubCommands, expectedSubCommands); - }); - }); -});