Skip to content

Commit 4bad969

Browse files
fix(core): address PR feedback by restricting env prefix and redirection to simple commands
1 parent 030c928 commit 4bad969

4 files changed

Lines changed: 78 additions & 76 deletions

File tree

packages/core/src/utils/shell-utils.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ describe('hasRedirection', () => {
289289
mockPlatform.mockReturnValue('linux');
290290
expect(hasRedirection('echo "a > b"')).toBe(false);
291291
});
292+
293+
it('should not detect redirection in chained commands', () => {
294+
expect(hasRedirection('cmd1 && echo hello > world')).toBe(false);
295+
expect(hasRedirection('echo hello > world || cmd2')).toBe(false);
296+
expect(hasRedirection('cmd1 ; echo hello > world')).toBe(false);
297+
});
292298
});
293299

294300
describeWindowsOnly('PowerShell integration', () => {
@@ -647,8 +653,9 @@ describe('hasEnvPrefix', () => {
647653
expect(hasEnvPrefix('echo "${FOO}"')).toBe(false);
648654
});
649655

650-
it('should not detect commands that just happen to have an equal sign later', () => {
651-
expect(hasEnvPrefix('echo foo=bar')).toBe(false);
652-
expect(hasEnvPrefix('cmd --opt=val')).toBe(false);
656+
it('should not detect environment variables in chained commands', () => {
657+
expect(hasEnvPrefix('FOO=bar cmd && cmd2')).toBe(false);
658+
expect(hasEnvPrefix('cmd1 || FOO=bar cmd2')).toBe(false);
659+
expect(hasEnvPrefix('cmd1 ; FOO=bar cmd2')).toBe(false);
653660
});
654661
});

packages/core/src/utils/shell-utils.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ interface CommandParseResult {
186186
details: ParsedCommandDetail[];
187187
hasError: boolean;
188188
hasRedirection?: boolean;
189+
hasEnvPrefix?: boolean;
189190
}
190191

191192
const POWERSHELL_COMMAND_ENV = '__GCLI_POWERSHELL_COMMAND__';
@@ -506,6 +507,8 @@ export function parseBashCommandDetails(
506507
return {
507508
details: details.sort((a, b) => a.startIndex - b.startIndex),
508509
hasError,
510+
hasRedirection: hasRedirection(command),
511+
hasEnvPrefix: hasEnvPrefix(command),
509512
};
510513
}
511514

@@ -729,22 +732,45 @@ export function hasRedirection(command: string): boolean {
729732
const tree = parseCommandTree(command);
730733
if (!tree) return fallbackCheck();
731734

732-
const stack: Node[] = [tree.rootNode];
735+
const root = tree.rootNode;
736+
// tree-sitter-bash wraps everything in a program/document root.
737+
// We check if the command is a simple, non-compound command.
738+
const stack: Node[] = [root];
733739
while (stack.length > 0) {
734740
const current = stack.pop()!;
735-
if (
736-
current.type === 'redirected_statement' ||
737-
current.type === 'file_redirect' ||
738-
current.type === 'heredoc_redirect' ||
739-
current.type === 'herestring_redirect'
740-
) {
741-
return true;
741+
if (current.type === 'list' || current.type === 'pipeline') {
742+
return false;
742743
}
743744
for (let i = current.childCount - 1; i >= 0; i -= 1) {
744745
const child = current.child(i);
745746
if (child) stack.push(child);
746747
}
747748
}
749+
750+
if (root.namedChildCount === 1) {
751+
const firstChild = root.namedChild(0);
752+
if (
753+
firstChild?.type === 'command' ||
754+
firstChild?.type === 'redirected_statement'
755+
) {
756+
const stack: Node[] = [firstChild];
757+
while (stack.length > 0) {
758+
const current = stack.pop()!;
759+
if (
760+
current.type === 'redirected_statement' ||
761+
current.type === 'file_redirect' ||
762+
current.type === 'heredoc_redirect' ||
763+
current.type === 'herestring_redirect'
764+
) {
765+
return true;
766+
}
767+
for (let i = current.childCount - 1; i >= 0; i -= 1) {
768+
const child = current.child(i);
769+
if (child) stack.push(child);
770+
}
771+
}
772+
}
773+
}
748774
return false;
749775
}
750776

@@ -765,18 +791,43 @@ export function hasEnvPrefix(command: string): boolean {
765791
const tree = parseCommandTree(command);
766792
if (!tree) return fallbackCheck();
767793

768-
const stack: Node[] = [tree.rootNode];
769-
while (stack.length > 0) {
770-
const current = stack.pop()!;
771-
if (current.type === 'variable_assignment') {
772-
return true;
773-
}
774-
if (current.type === 'command_name' && current.text === 'env') {
775-
return true;
794+
// Check if the root is a single command. If it's a compound command
795+
// (pipeline, list, etc.), we return false because we only want to allow
796+
// environment prefixes for simple, standalone commands.
797+
const root = tree.rootNode;
798+
// tree-sitter-bash wraps everything in a program/document root.
799+
const compoundStack: Node[] = [root];
800+
while (compoundStack.length > 0) {
801+
const current = compoundStack.pop()!;
802+
if (current.type === 'list' || current.type === 'pipeline') {
803+
return false;
776804
}
777805
for (let i = current.childCount - 1; i >= 0; i -= 1) {
778806
const child = current.child(i);
779-
if (child) stack.push(child);
807+
if (child) compoundStack.push(child);
808+
}
809+
}
810+
811+
if (root.namedChildCount === 1) {
812+
const firstChild = root.namedChild(0);
813+
if (
814+
firstChild?.type === 'command' ||
815+
firstChild?.type === 'variable_assignment'
816+
) {
817+
const stack: Node[] = [firstChild];
818+
while (stack.length > 0) {
819+
const current = stack.pop()!;
820+
if (current.type === 'variable_assignment') {
821+
return true;
822+
}
823+
if (current.type === 'command_name' && current.text === 'env') {
824+
return true;
825+
}
826+
for (let i = current.childCount - 1; i >= 0; i -= 1) {
827+
const child = current.child(i);
828+
if (child) stack.push(child);
829+
}
830+
}
780831
}
781832
}
782833
return false;

test_parse_tree3.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

test_parse_tree4.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)