From af56cb78f940148f1619e81640065f7f3556f801 Mon Sep 17 00:00:00 2001 From: arimu1 Date: Sat, 25 Jul 2026 10:42:29 +0700 Subject: [PATCH 1/2] fix(glob): support path separators in glob patterns on Windows (#2067) In glob mode the regex is extracted straight out of globset, where `/` is the literal separator. walk.rs then matches it against the raw native path, which on Windows still contains backslashes, so any pattern with a separator matched nothing. Normalize the pattern to `/` on Windows and rewrite the generated regex so either separator is accepted. The rewrite has to be class-aware: a naive `replace('/', ...)` corrupts the `[^/]` classes that globset emits for `*` and `?`, producing a regex that requires a literal `]`. --- CHANGELOG.md | 1 + src/main.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b09d9f3..9c27b581f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fire the "search pattern contains a path separator" diagnostic for any pattern containing `/`, not just patterns that happen to name an existing directory. Preserves the legacy Windows behaviour that also flags native `\` separators when the pattern resolves to a real directory. See #1873. - Also fire the "search pattern contains a path separator" diagnostic for `--and` patterns, not only the primary positional pattern. `--and` patterns are matched against the file name just like the primary pattern, so a path separator in them silently returned zero results. See #1873. - Fix bug where passing "-" as a directory argument didn't actually search that directory, see #849 (@Sean-Kenneth-Doherty). +- Fix `--glob` patterns containing a path separator never matching on Windows, where candidate paths keep their backslashes at match time, see #2067. # 10.4.2 diff --git a/src/main.rs b/src/main.rs index 609078b2b..010404d28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -215,10 +215,60 @@ fn ensure_single_search_pattern_is_not_a_path(pattern: &str) -> Result<()> { } } +/// Rewrite a globset-generated regex so that either `/` or `\` is accepted wherever +/// globset emitted a path separator. Windows candidate paths keep their backslashes +/// at match time, so a regex that only accepts `/` never matches. +/// +/// Separators inside a character class have to be handled differently from the ones +/// outside it: inside, the backslash joins the existing members (`[^/]` -> `[^/\\]`), +/// outside, the separator becomes a class of its own (`a/b` -> `a[/\\]b`). +fn accept_either_separator(regex: &str) -> String { + let mut out = String::with_capacity(regex.len()); + let mut chars = regex.chars(); + let mut in_class = false; + + while let Some(c) = chars.next() { + match c { + // Keep escape sequences intact so an escaped `[` doesn't open a class. + '\\' => { + out.push(c); + if let Some(escaped) = chars.next() { + out.push(escaped); + } + } + '[' if !in_class => { + in_class = true; + out.push(c); + } + ']' if in_class => { + in_class = false; + out.push(c); + } + '/' if in_class => out.push_str("/\\\\"), + '/' => out.push_str("[/\\\\]"), + _ => out.push(c), + } + } + + out +} + fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result { Ok(if opts.glob && !pattern.is_empty() { - let glob = GlobBuilder::new(pattern).literal_separator(true).build()?; - glob.regex().to_owned() + let pattern_str = if cfg!(windows) { + pattern.replace('\\', "/") + } else { + pattern.to_string() + }; + let glob = GlobBuilder::new(&pattern_str) + .literal_separator(true) + .build()?; + let regex_str = glob.regex(); + if cfg!(windows) { + accept_either_separator(regex_str) + } else { + regex_str.to_owned() + } } else if opts.exact { // Anchor the escaped pattern so the full filename (or path) must match exactly. // Literal. No substring matching. @@ -553,3 +603,41 @@ fn build_regex(pattern_regex: String, config: &Config) -> Result Date: Sun, 2 Aug 2026 17:12:25 +0700 Subject: [PATCH 2/2] style: silence clippy useless_borrows_in_formatting in tests Named format args no longer need an extra `&` under -Dwarnings. --- tests/tests.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 191447b5e..0fce5ed35 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1274,7 +1274,7 @@ fn test_absolute_path() { {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo/ {abs_path}/symlink", - abs_path = &abs_path + abs_path = abs_path ), ); @@ -1287,7 +1287,7 @@ fn test_absolute_path() { {abs_path}/one/two/C.Foo2 {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo/", - abs_path = &abs_path + abs_path = abs_path ), ); } @@ -1306,7 +1306,7 @@ fn test_implicit_absolute_path() { {abs_path}/one/two/C.Foo2 {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo/", - abs_path = &abs_path + abs_path = abs_path ), ); } @@ -1326,7 +1326,7 @@ fn test_normalized_absolute_path() { {abs_path}/one/two/C.Foo2 {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo/", - abs_path = &abs_path + abs_path = abs_path ), ); } @@ -1560,7 +1560,7 @@ fn test_symlink_as_root() { {dir}/one/two/three/d.foo {dir}/one/two/three/directory_foo/ {dir}/symlink", - dir = &parent_parent + dir = parent_parent ), ); } @@ -1580,7 +1580,7 @@ fn test_symlink_and_absolute_path() { {abs_path}/{expected_path}/three/ {abs_path}/{expected_path}/three/d.foo {abs_path}/{expected_path}/three/directory_foo/", - abs_path = &abs_path, + abs_path = abs_path, expected_path = expected_path ), ); @@ -1598,7 +1598,7 @@ fn test_symlink_as_absolute_root() { {abs_path}/symlink/three/ {abs_path}/symlink/three/d.foo {abs_path}/symlink/three/directory_foo/", - abs_path = &abs_path + abs_path = abs_path ), ); } @@ -1622,7 +1622,7 @@ fn test_symlink_and_full_path() { "{abs_path}/{expected_path}/three/ {abs_path}/{expected_path}/three/d.foo {abs_path}/{expected_path}/three/directory_foo/", - abs_path = &abs_path, + abs_path = abs_path, expected_path = expected_path ), ); @@ -1643,7 +1643,7 @@ fn test_symlink_and_full_path_abs_path() { "{abs_path}/symlink/three/ {abs_path}/symlink/three/d.foo {abs_path}/symlink/three/directory_foo/", - abs_path = &abs_path + abs_path = abs_path ), ); } @@ -1772,7 +1772,7 @@ fn test_exec() { {abs_path}/one/two/c.foo {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo", - abs_path = &abs_path + abs_path = abs_path ), ); @@ -1871,7 +1871,7 @@ fn test_exec_multi() { test c.foo test d.foo test directory_foo", - abs_path = &abs_path + abs_path = abs_path ), ); @@ -1927,7 +1927,7 @@ fn test_exec_batch() { &["--absolute-path", "foo", "--exec-batch", "echo"], &format!( "{abs_path}/a.foo {abs_path}/one/b.foo {abs_path}/one/two/C.Foo2 {abs_path}/one/two/c.foo {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo", - abs_path = &abs_path + abs_path = abs_path ), ); @@ -2539,7 +2539,7 @@ fn test_base_directory() { // Ignore base directory when absolute path is used let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES); - let abs_base_dir = &format!("{abs_path}/one/two/", abs_path = &abs_path); + let abs_base_dir = &format!("{abs_path}/one/two/", abs_path = abs_path); te.assert_output( &["--base-directory", abs_base_dir, "foo", &abs_path], &format!( @@ -2549,7 +2549,7 @@ fn test_base_directory() { {abs_path}/one/two/C.Foo2 {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo/", - abs_path = &abs_path + abs_path = abs_path ), ); }