Move tree sitter parsing into own class

This commit is contained in:
Daniel Imms
2025-10-21 09:01:44 -07:00
parent 9013d2a9ec
commit f4edae643b
6 changed files with 70 additions and 586 deletions

View File

@@ -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'

View File

@@ -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() {

View File

@@ -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);
}

View File

@@ -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<string, IToolTerminal> = 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');
}
}
}

View File

@@ -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<Parser>;
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<Promise<Query>> };
constructor(
@ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService
) {
this._parser = this._treeSitterLibraryService.getParserClass().then(ParserCtor => new ParserCtor());
}
async extractSubCommands(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise<string[]> {
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;
}
}

View File

@@ -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);
});
});
});