@@ -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
149149fn 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+
166208fn 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