diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index e35ebbbb..31ee76af 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -444,6 +444,42 @@ fn get_or_create_file(path: &str) -> Result> { Ok(file) } +/// GNU find warns if a pattern that is matched against basenames only +/// (the argument to -name/-iname) contains a directory separator, because +/// such a pattern can never match. Returns the warning message, if any. +fn basename_pattern_warning(option_name: &str, pattern: &str) -> Option { + if pattern.contains('/') && pattern != "/" { + Some(format!( + "‘{option_name}’ matches against basenames only, but the given \ + pattern contains a directory separator (‘/’), thus the \ + expression will evaluate to false all the time. Did you mean \ + ‘-wholename’?" + )) + } else { + None + } +} + +/// GNU find warns if a pattern that is matched against the full path (the +/// argument to -path/-ipath/-wholename/-iwholename) ends with a directory +/// separator, because such a pattern can never match. Returns the warning +/// message, if any. +fn fullpath_pattern_warning(option_name: &str, pattern: &str) -> Option { + if pattern.ends_with('/') && pattern != "/" { + Some(format!( + "{option_name} {pattern} will not match anything because it ends with /." + )) + } else { + None + } +} + +fn issue_warning(warning: Option) { + if let Some(warning) = warning { + eprintln!("find: warning: {warning}"); + } +} + /// The main "translate command-line args into a matcher" function. Will call /// itself recursively if it encounters an opening bracket. A successful return /// consists of a tuple containing the new index into the args array to use (if @@ -530,6 +566,7 @@ fn build_matcher_tree( return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; + issue_warning(basename_pattern_warning(args[i - 1], args[i])); Some(NameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } "-path" | "-ipath" | "-wholename" | "-iwholename" => { @@ -537,6 +574,7 @@ fn build_matcher_tree( return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; + issue_warning(fullpath_pattern_warning(args[i - 1], args[i])); Some(PathMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } "-readable" => Some(AccessMatcher::Readable.into_box()), diff --git a/tests/test_find.rs b/tests/test_find.rs index ce223d56..2b5c5de2 100644 --- a/tests/test_find.rs +++ b/tests/test_find.rs @@ -1153,6 +1153,87 @@ fn find_slashes() { .no_stderr(); } +#[test] +fn find_name_pattern_with_directory_separator_warns() { + ucmd() + .args(&["./test_data/simple", "-name", "/a.txt"]) + .succeeds() + .stderr_only("find: warning: ‘-name’ matches against basenames only, but the given pattern contains a directory separator (‘/’), thus the expression will evaluate to false all the time. Did you mean ‘-wholename’?\n"); +} + +#[test] +fn find_iname_pattern_with_directory_separator_warns() { + ucmd() + .args(&["./test_data/simple", "-iname", "foo/bar"]) + .succeeds() + .stderr_contains("‘-iname’ matches against basenames only") + .stderr_contains("Did you mean ‘-wholename’?") + .no_stdout(); +} + +#[test] +fn find_wholename_pattern_with_trailing_separator_warns() { + ucmd() + .args(&["./test_data/simple", "-wholename", "a.txt/"]) + .succeeds() + .stderr_only( + "find: warning: -wholename a.txt/ will not match anything because it ends with /.\n", + ); +} + +#[test] +fn find_path_pattern_with_trailing_separator_warns() { + ucmd() + .args(&["./test_data/simple", "-path", "./"]) + .succeeds() + .stderr_only("find: warning: -path ./ will not match anything because it ends with /.\n"); +} + +#[test] +fn find_ipath_pattern_with_trailing_separator_warns() { + ucmd() + .args(&["./test_data/simple", "-ipath", "foo/"]) + .succeeds() + .stderr_contains("-ipath foo/ will not match anything because it ends with /.") + .no_stdout(); +} + +#[test] +fn find_iwholename_pattern_with_trailing_separator_warns() { + ucmd() + .args(&["./test_data/simple", "-iwholename", "foo/"]) + .succeeds() + .stderr_contains("-iwholename foo/ will not match anything because it ends with /.") + .no_stdout(); +} + +#[test] +fn find_pattern_warnings_are_issued_for_every_occurrence() { + let warning = "find: warning: ‘-name’ matches against basenames only, but the given pattern contains a directory separator (‘/’), thus the expression will evaluate to false all the time. Did you mean ‘-wholename’?\n"; + ucmd() + .args(&["./test_data/simple", "-name", "a/b", "-o", "-name", "c/d"]) + .succeeds() + .stderr_only(format!("{warning}{warning}")); +} + +#[test] +fn find_pattern_warning_does_not_suppress_normal_output() { + ucmd() + .args(&[ + "./test_data/simple", + "-maxdepth", + "1", + "-name", + "a/b", + "-o", + "-name", + "abbbc", + ]) + .succeeds() + .stderr_contains("matches against basenames only") + .stdout_contains("abbbc"); +} + // -ok / -okdir integration tests // // These tests use pipe_in() to supply the user's response. Because pipe_in()