diff --git a/src/lib.rs b/src/lib.rs index 3ef5091..d683595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -905,6 +905,10 @@ fn fill_todo( }; let pattern = &patterns[idx]; + // Whether this component's pattern starts with a literal `.`, in which case + // it is allowed to match entries whose name starts with a `.` even when + // `require_literal_leading_dot` is set. + let has_literal_leading_dot = matches!(pattern.tokens.first(), Some(Char('.'))); let is_dir = path.is_directory; let curdir = path.as_ref() == Path::new("."); match (pattern.has_metachars, is_dir) { @@ -960,7 +964,15 @@ fn fill_todo( }); match dirs { Ok(mut children) => { - if options.require_literal_leading_dot { + // Entries whose name starts with a `.` are skipped here so + // that a recursive `**` component, which matches directory + // entries without ever consulting the pattern, does not + // descend into them. Patterns that do start with a literal + // `.` must not be filtered: `Pattern::matches_with` already + // implements `require_literal_leading_dot` faithfully for + // them, and dropping the entries here would make e.g. + // `.git*` match nothing at all. + if options.require_literal_leading_dot && !has_literal_leading_dot { children.retain(|x| !x.1.to_str().unwrap().starts_with('.')); } children.sort_by(|p1, p2| p2.1.cmp(&p1.1)); @@ -971,7 +983,7 @@ fn fill_todo( // requires that the pattern has a leading dot, even if the // `MatchOptions` field `require_literal_leading_dot` is not // set. - if !pattern.tokens.is_empty() && pattern.tokens[0] == Char('.') { + if has_literal_leading_dot { for &special in &[".", ".."] { if pattern.matches_with(special, options) { add(todo, PathWrapper::from_path(path.join(special))); diff --git a/tests/glob-std.rs b/tests/glob-std.rs index f8c5379..510cf18 100644 --- a/tests/glob-std.rs +++ b/tests/glob-std.rs @@ -384,6 +384,31 @@ fn main() { vec!(PathBuf::from("i/qwe"), PathBuf::from("i/qwe/eee")) ); + // `require_literal_leading_dot` only requires that the leading `.` appears + // literally in the pattern -- it must not stop such a pattern from matching. + // This mirrors what `Pattern::matches_with` does for the same options. + mk_file("j", true); + mk_file("j/.aaa", false); + mk_file("j/.abb", false); + mk_file("j/bbb", false); + + assert_eq!( + glob_with_vec("j/.a*", options), + vec!(PathBuf::from("j/.aaa"), PathBuf::from("j/.abb")) + ); + assert_eq!( + glob_with_vec("j/.?aa", options), + vec!(PathBuf::from("j/.aaa")) + ); + assert_eq!( + glob_with_vec("j/.[ab]bb", options), + vec!(PathBuf::from("j/.abb")) + ); + // ... while patterns that do not start with a literal `.` keep skipping + // those entries. + assert_eq!(glob_with_vec("j/*", options), vec!(PathBuf::from("j/bbb"))); + assert_eq!(glob_with_vec("j/?aa", options), Vec::::new()); + if env::consts::FAMILY != "windows" { assert_eq!( glob_vec("bbb/specials/[*]"),