Skip to content

Commit 32fcbed

Browse files
committed
Fix: Improve path separator detection to avoid false positives with regex patterns
The previous fix was too aggressive and flagged valid regex patterns like \Ac on Windows (where \A is a regex anchor). This change adds a heuristic to distinguish between paths and regex escape sequences: - On Windows: Only flag patterns that look like paths (drive paths like C:\, or patterns with backslashes that aren't short regex escapes like \Ac) - On Unix: Continue to flag all patterns with path separators This fixes the test_smart_case failure while still addressing issue #1873.
1 parent 99d0194 commit 32fcbed

1 file changed

Lines changed: 52 additions & 10 deletions

File tree

src/main.rs

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,21 +148,63 @@ fn set_working_dir(opts: &Opts) -> Result<()> {
148148
/// Detect if the user accidentally supplied a path instead of a search pattern
149149
fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
150150
if !opts.full_path && opts.pattern.contains(std::path::MAIN_SEPARATOR) {
151-
Err(anyhow!(
152-
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
153-
and will not lead to any search results.\n\n\
154-
If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \
155-
fd . '{pattern}'\n\n\
156-
Instead, if you want your pattern to match the full file path, use:\n\n \
157-
fd --full-path '{pattern}'",
158-
pattern = &opts.pattern,
159-
sep = std::path::MAIN_SEPARATOR,
160-
))
151+
// On Windows, backslash is both a path separator and a regex escape character.
152+
// We need to distinguish between paths (e.g., "C:\path" or "\nonexistent") and
153+
// regex patterns (e.g., "\Ac" where \A is a regex anchor).
154+
// A simple heuristic: if the pattern looks like it could be a path (not just
155+
// a single-character regex escape), show the error.
156+
let looks_like_path = if cfg!(windows) {
157+
// On Windows, check if it's a drive path (C:\) or if the backslash is
158+
// followed by something that looks like a path component (not a single regex escape)
159+
let is_drive_path = opts.pattern.len() >= 3
160+
&& opts.pattern.chars().next().map_or(false, |c| c.is_ascii_alphabetic())
161+
&& opts.pattern.chars().nth(1) == Some(':')
162+
&& opts.pattern.chars().nth(2) == Some(std::path::MAIN_SEPARATOR);
163+
is_drive_path
164+
|| (opts.pattern.matches(std::path::MAIN_SEPARATOR).count() > 0
165+
&& !is_likely_regex_escape(&opts.pattern))
166+
} else {
167+
// On Unix, if it starts with / or contains /, it's likely a path
168+
true
169+
};
170+
171+
if looks_like_path {
172+
Err(anyhow!(
173+
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
174+
and will not lead to any search results.\n\n\
175+
If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \
176+
fd . '{pattern}'\n\n\
177+
Instead, if you want your pattern to match the full file path, use:\n\n \
178+
fd --full-path '{pattern}'",
179+
pattern = &opts.pattern,
180+
sep = std::path::MAIN_SEPARATOR,
181+
))
182+
} else {
183+
Ok(())
184+
}
161185
} else {
162186
Ok(())
163187
}
164188
}
165189

190+
/// Check if a pattern is likely a regex escape sequence rather than a path.
191+
/// This is a heuristic to avoid false positives on Windows where \ is both
192+
/// a path separator and a regex escape character.
193+
fn is_likely_regex_escape(pattern: &str) -> bool {
194+
if !cfg!(windows) {
195+
return false;
196+
}
197+
// Common regex escape sequences: \A, \z, \b, \d, \s, \w, \1, \2, etc.
198+
// If the pattern is very short (like "\Ac") and starts with \ followed by
199+
// a letter or digit, it's likely a regex escape.
200+
if pattern.len() <= 3 && pattern.starts_with('\\') {
201+
if let Some(ch) = pattern.chars().nth(1) {
202+
return ch.is_ascii_alphanumeric();
203+
}
204+
}
205+
false
206+
}
207+
166208
fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> {
167209
Ok(if opts.glob && !pattern.is_empty() {
168210
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;

0 commit comments

Comments
 (0)