From 46621e07c9219f6085ecdc6fa1adbaddcff5cc79 Mon Sep 17 00:00:00 2001 From: dchaudhari7177 <111210939+dchaudhari7177@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:41:58 +0530 Subject: [PATCH 1/2] fix(grep): keep the glob attached to --include so MSYS cannot expand it grep passed the filter as two arguments, ["--include", glob]. On Windows the grep found on PATH is normally Git for Windows' MSYS build, and its runtime glob-expands a bare "*" argument against the *current* directory before grep parses it. The filter then names whatever file sorted first in the cwd, so a search of any other directory silently reports "No matches". That is the whole tool failing, not an edge case: with Git installed, shutil.which("grep") finds it, so every grep call takes this path. Reproduced directly -- cwd holding one unrelated file, searching a temp tree that contains two matches: --include * rc=1 matches=0 <- expansion hits the cwd --include=* rc=0 matches=2 omitted rc=0 matches=2 Attaching the glob to the flag hides it from that expansion, and is equivalent everywhere else. Filtering is unaffected: --include=*.py still selects only .py files. This is why tests/test_tools.py::test_grep and ::test_grep_ignore_case fail on a Windows checkout of main. Tests: the two existing greps now pass; three added. Two assert the command shape rather than the result, because both spellings behave identically without the MSYS runtime -- on a Linux runner a revert would otherwise stay green. The third checks end-to-end that a glob still filters. --- gcode/tools.py | 8 ++++++- tests/test_tools.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/gcode/tools.py b/gcode/tools.py index 299ac6d..189de9a 100644 --- a/gcode/tools.py +++ b/gcode/tools.py @@ -219,7 +219,13 @@ def grep( flags = ["-rnI"] if ignore_case: flags.append("-i") - cmd = [grep_bin, *flags, "--include", glob, "-e", pattern, path] + # --include=, not --include : on Windows the grep on PATH + # is usually Git for Windows' MSYS build, whose runtime glob-expands a + # bare "*" argument against the *current* directory before grep sees + # it. The filter then names whatever file happened to sort first here, + # so a search of some other directory quietly matches nothing. Keeping + # the glob attached to the flag hides it from that expansion. + cmd = [grep_bin, *flags, f"--include={glob}", "-e", pattern, path] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, check=False) except subprocess.TimeoutExpired: diff --git a/tests/test_tools.py b/tests/test_tools.py index a9ac302..791e129 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -154,3 +154,54 @@ def test_execute_bash_auto_approve_skips_prompt(tmp_path): assert "auto-approved" in out finally: set_auto_approve(AUTO_APPROVE) + + +def test_grep_passes_include_as_one_argument(): + """The glob must stay attached to --include, as --include=. + + On Windows the grep on PATH is usually Git for Windows' MSYS build, whose + runtime glob-expands a bare "*" argument against the current directory + before grep sees it -- so `--include", "*"` becomes `--include ` and a search of any other directory matches nothing. + + Both spellings behave identically where that runtime is not involved, so + this asserts the command shape rather than the result: on a Linux runner a + revert would otherwise stay green. + """ + with patch("gcode.tools.shutil.which", return_value="/usr/bin/grep"), patch( + "gcode.tools.subprocess.run" + ) as run: + run.return_value.returncode = 0 + run.return_value.stdout = "" + run.return_value.stderr = "" + + grep.invoke({"pattern": "needle", "path": ".", "glob": "*.py"}) + + cmd = run.call_args[0][0] + assert "--include=*.py" in cmd + assert "--include" not in cmd, "the glob must not be a separate argument" + + +def test_grep_include_defaults_to_everything(): + """The default glob is still passed, so behaviour is unchanged.""" + with patch("gcode.tools.shutil.which", return_value="/usr/bin/grep"), patch( + "gcode.tools.subprocess.run" + ) as run: + run.return_value.returncode = 1 + run.return_value.stdout = "" + run.return_value.stderr = "" + + grep.invoke({"pattern": "needle", "path": "."}) + + assert "--include=*" in run.call_args[0][0] + + +def test_grep_filters_by_glob(tmp_path): + """End-to-end: the glob still selects files rather than being ignored.""" + (tmp_path / "a.txt").write_text("needle in text\n") + (tmp_path / "b.py").write_text("needle in python\n") + + out = grep.invoke({"pattern": "needle", "path": str(tmp_path), "glob": "*.py"}) + + assert "needle in python" in out + assert "needle in text" not in out From 34c5f282af9469eb53801f3935adc9841a954f60 Mon Sep 17 00:00:00 2001 From: ShauryaGangrade Date: Fri, 14 Aug 2026 23:01:31 +0530 Subject: [PATCH 2/2] style: ruff format on new grep include tests --- tests/test_tools.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 791e129..ea7b625 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -168,9 +168,10 @@ def test_grep_passes_include_as_one_argument(): this asserts the command shape rather than the result: on a Linux runner a revert would otherwise stay green. """ - with patch("gcode.tools.shutil.which", return_value="/usr/bin/grep"), patch( - "gcode.tools.subprocess.run" - ) as run: + with ( + patch("gcode.tools.shutil.which", return_value="/usr/bin/grep"), + patch("gcode.tools.subprocess.run") as run, + ): run.return_value.returncode = 0 run.return_value.stdout = "" run.return_value.stderr = "" @@ -184,9 +185,10 @@ def test_grep_passes_include_as_one_argument(): def test_grep_include_defaults_to_everything(): """The default glob is still passed, so behaviour is unchanged.""" - with patch("gcode.tools.shutil.which", return_value="/usr/bin/grep"), patch( - "gcode.tools.subprocess.run" - ) as run: + with ( + patch("gcode.tools.shutil.which", return_value="/usr/bin/grep"), + patch("gcode.tools.subprocess.run") as run, + ): run.return_value.returncode = 1 run.return_value.stdout = "" run.return_value.stderr = ""