Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/find/matchers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,42 @@ fn get_or_create_file(path: &str) -> Result<File, Box<dyn Error>> {
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<String> {
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<String> {
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<String>) {
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
Expand Down Expand Up @@ -530,13 +566,15 @@ 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" => {
if i >= args.len() - 1 {
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()),
Expand Down
81 changes: 81 additions & 0 deletions tests/test_find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading