Skip to content

Commit aa97a1a

Browse files
authored
Fix BSD grep regex failures with ERE fallback
- retry grep with -E when BSD basic-regex parsing fails\n- catch unmatched brace error variants (e.g. 'Unmatched {')\n- add regression test for shell-parameter style pattern\n- keep no-match behavior unchanged
1 parent cd39ea8 commit aa97a1a

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

src/tools/defaults.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,37 @@ export function registerDefaultTools(registry: ToolRegistry): void {
247247
}
248248
grepArgs.push(pattern, path);
249249

250+
const runGrep = async (extraArgs: string[] = []) => {
251+
return execFileAsync("grep", [...extraArgs, ...grepArgs], { timeout: 30000 });
252+
};
253+
250254
try {
251-
const { stdout } = await execFileAsync("grep", grepArgs, { timeout: 30000 });
255+
const { stdout } = await runGrep();
252256
return stdout || "No matches found";
253257
} catch (error: any) {
254258
// grep exits with code 1 when no matches found — not an error
255259
if (error.code === 1) {
256260
return "No matches found";
257261
}
262+
263+
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
264+
const isRegexSyntaxError = error.code === 2
265+
&& /(invalid regular expression|invalid repetition count|braces not balanced|repetition-operator operand invalid|unmatched(\s*\\?\{)?)/i.test(stderr);
266+
267+
// BSD grep uses basic regex by default and can reject patterns that work in ERE.
268+
// Retry with -E so patterns like \$\{[A-Z_][A-Z0-9_]*:- are handled.
269+
if (isRegexSyntaxError) {
270+
try {
271+
const { stdout } = await runGrep(["-E"]);
272+
return stdout || "No matches found";
273+
} catch (extendedError: any) {
274+
if (extendedError.code === 1) {
275+
return "No matches found";
276+
}
277+
throw extendedError;
278+
}
279+
}
280+
258281
throw error;
259282
}
260283
});

tests/tools/defaults.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,28 @@ describe("Default Tools", () => {
251251
fs.unlinkSync(tmpFile);
252252
});
253253

254+
it("should retry grep with extended regex when BSD basic regex rejects pattern", async () => {
255+
const registry = new ToolRegistry();
256+
registerDefaultTools(registry);
257+
const executor = new LocalExecutor(registry);
258+
259+
const fs = await import("fs");
260+
const os = await import("os");
261+
const path = await import("path");
262+
const tmpFile = path.join(os.tmpdir(), `test-grep-bsd-${Date.now()}.txt`);
263+
fs.writeFileSync(tmpFile, "export DEFAULT=${MY_VAR:-fallback}\n", "utf-8");
264+
265+
const result = await executeWithChain([executor], "grep", {
266+
pattern: "\\$\\{[A-Z_][A-Z0-9_]*:-",
267+
path: tmpFile,
268+
});
269+
270+
expect(result.status).toBe("success");
271+
expect(result.output).toContain("DEFAULT=${MY_VAR:-fallback}");
272+
273+
fs.unlinkSync(tmpFile);
274+
});
275+
254276
it("should prevent grep command injection via pattern", async () => {
255277
const registry = new ToolRegistry();
256278
registerDefaultTools(registry);

0 commit comments

Comments
 (0)