mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-29 07:21:27 -07:00
fix(core): switch to subshells for shell tool wrapping to fix heredocs and edge cases (#24024)
This commit is contained in:
@@ -277,7 +277,7 @@ describe('ShellTool', () => {
|
||||
|
||||
const result = await promise;
|
||||
|
||||
const wrappedCommand = `{ my-command & }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
const wrappedCommand = `(\n${'my-command &'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
tempRootDir,
|
||||
@@ -295,6 +295,42 @@ describe('ShellTool', () => {
|
||||
expect(fs.existsSync(tmpFile)).toBe(false);
|
||||
});
|
||||
|
||||
it('should add a space when command ends with a backslash to prevent escaping newline', async () => {
|
||||
const invocation = shellTool.build({ command: 'ls\\' });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
resolveShellExecution();
|
||||
await promise;
|
||||
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
|
||||
const wrappedCommand = `(\nls\\ \n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
tempRootDir,
|
||||
expect.any(Function),
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle trailing comments correctly by placing them on their own line', async () => {
|
||||
const invocation = shellTool.build({ command: 'ls # comment' });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
resolveShellExecution();
|
||||
await promise;
|
||||
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
|
||||
const wrappedCommand = `(\nls # comment\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
tempRootDir,
|
||||
expect.any(Function),
|
||||
expect.any(AbortSignal),
|
||||
false,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the provided absolute directory as cwd', async () => {
|
||||
const subdir = path.join(tempRootDir, 'subdir');
|
||||
const invocation = shellTool.build({
|
||||
@@ -306,7 +342,7 @@ describe('ShellTool', () => {
|
||||
await promise;
|
||||
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
|
||||
const wrappedCommand = `{ ls; }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
const wrappedCommand = `(\n${'ls'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
subdir,
|
||||
@@ -331,7 +367,7 @@ describe('ShellTool', () => {
|
||||
await promise;
|
||||
|
||||
const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp');
|
||||
const wrappedCommand = `{ ls; }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
const wrappedCommand = `(\n${'ls'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`;
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
wrappedCommand,
|
||||
path.join(tempRootDir, 'subdir'),
|
||||
|
||||
@@ -76,6 +76,33 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
super(params, messageBus, _toolName, _toolDisplayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a command in a subshell `()` to capture background process IDs (PIDs) using pgrep.
|
||||
* Uses newlines to prevent breaking heredocs or trailing comments.
|
||||
*
|
||||
* @param command The raw command string to execute.
|
||||
* @param tempFilePath Path to the temporary file where PIDs will be written.
|
||||
* @param isWindows Whether the current platform is Windows (if true, the command is returned as-is).
|
||||
* @returns The wrapped command string.
|
||||
*/
|
||||
private wrapCommandForPgrep(
|
||||
command: string,
|
||||
tempFilePath: string,
|
||||
isWindows: boolean,
|
||||
): string {
|
||||
if (isWindows) {
|
||||
return command;
|
||||
}
|
||||
let trimmed = command.trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
if (trimmed.endsWith('\\')) {
|
||||
trimmed += ' ';
|
||||
}
|
||||
return `(\n${trimmed}\n); __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`;
|
||||
}
|
||||
|
||||
private getContextualDetails(): string {
|
||||
let details = '';
|
||||
// append optional [in directory]
|
||||
@@ -232,14 +259,11 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
|
||||
try {
|
||||
// pgrep is not available on Windows, so we can't get background PIDs
|
||||
const commandToExecute = isWindows
|
||||
? strippedCommand
|
||||
: (() => {
|
||||
// wrap command to append subprocess pids (via pgrep) to temporary file
|
||||
let command = strippedCommand.trim();
|
||||
if (!command.endsWith('&')) command += ';';
|
||||
return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`;
|
||||
})();
|
||||
const commandToExecute = this.wrapCommandForPgrep(
|
||||
strippedCommand,
|
||||
tempFilePath,
|
||||
isWindows,
|
||||
);
|
||||
|
||||
const cwd = this.params.dir_path
|
||||
? path.resolve(this.context.config.getTargetDir(), this.params.dir_path)
|
||||
|
||||
Reference in New Issue
Block a user