globmatch/
lib.rs

1#![warn(missing_debug_implementations, rust_2018_idioms, missing_docs)]
2
3//! This crate provides cross platform matching for globs with relative path prefixes.
4//!
5//! For CLI utilities it can be a common pattern to operate on a set of files. Such a set of files
6//! is either provided directly, as parameter to the tool - or via configuration files. The use of
7//! a configuration file makes it easier to determine the location of a file since the path
8//! can be specified relative to the configuration. Consider, e.g., the following `.json` input:
9//!
10//! ```ignore
11//! {
12//!   "globs": [
13//!     "../../../some/text-files/**/*.txt",
14//!     "other/inputs/*.md",
15//!     "paths/from/dir[0-9]/*.*"
16//!   ]
17//! }
18//! ```
19//!
20//! Specifying these paths in a dedicated configuration file allows to resolve the paths
21//! independent of the invocation of the script operating on these files, the location of the
22//! configuration file is used as base directory.
23//!
24//! This crate combines the features of the existing crates [globset][globset] and
25//! [walkdir][walkdir] to implement a *relative glob matcher*:
26//!
27//! - A [`Builder`] is created for each glob in the same style as in `globset::Glob`.
28//! - A [`Matcher`] is created from the [`Builder`] using [`Builder::build`]. This call resolves
29//!   the relative path components within the glob by "moving" it to the specified root directory.
30//! - The [`Matcher`] is then transformed into an iterator yielding `path::PathBuf`.
31//!
32//! For the previous example it would be sufficient to use one builder per glob and to specify
33//! the root folder when building the pattern (see examples below).
34//!
35//! # Globs
36//!
37//! Please check the documentation of [globset][globset] for the available glob format.
38//!
39//! # Example: A simple match.
40//!
41//! The following example uses the files stored in the `test-files/c-simple` folder, we're trying to match
42//! all the `.txt` files using the glob `test-files/c-simple/**/*.txt` (where `test-files/c-simple` is the only
43//! relative path component).
44//!
45//! ```
46//! /*
47//!     Example files:
48//!     globmatch/test-files/c-simple/.hidden
49//!     globmatch/test-files/c-simple/.hidden/h_1.txt
50//!     globmatch/test-files/c-simple/.hidden/h_0.txt
51//!     globmatch/test-files/c-simple/a/a2/a2_0.txt
52//!     globmatch/test-files/c-simple/a/a0/a0_0.txt
53//!     globmatch/test-files/c-simple/a/a0/a0_1.txt
54//!     globmatch/test-files/c-simple/a/a0/A0_3.txt
55//!     globmatch/test-files/c-simple/a/a0/a0_2.md
56//!     globmatch/test-files/c-simple/a/a1/a1_0.txt
57//!     globmatch/test-files/c-simple/some_file.txt
58//!     globmatch/test-files/c-simple/b/b_0.txt
59//!  */
60//!
61//! use globmatch;
62//!
63//! # fn example_a() -> Result<(), String> {
64//! let builder = globmatch::Builder::new("test-files/c-simple/**/*.txt")
65//!     .build(env!("CARGO_MANIFEST_DIR"))?;
66//!
67//! let paths: Vec<_> = builder.into_iter()
68//!     .flatten()
69//!     .collect();
70//!
71//! println!(
72//!     "paths:\n{}",
73//!     paths
74//!         .iter()
75//!         .map(|p| format!("{}", p.to_string_lossy()))
76//!         .collect::<Vec<_>>()
77//!         .join("\n")
78//! );
79//!
80//! assert_eq!(6 + 2 + 1, paths.len());
81//! # Ok(())
82//! # }
83//! # example_a().unwrap();
84//! ```
85//!
86//! # Example: Specifying options and using `.filter_entry`.
87//!
88//! Similar to the builder pattern in [globset][globset] when using `globset::GlobBuilder`, this
89//! crate allows to pass options (currently just case sensitivity) to the builder.
90//!
91//! In addition, the [`filter_entry`][filter_entry] function from [walkdir][walkdir] is accessible,
92//! but only as a single call (this crate does not implement a recursive iterator). This function
93//! allows filter files and folders *before* matching against the provided glob and therefore
94//! to efficiently exclude files and folders, e.g., hidden folders:
95//!
96//! ```
97//! use globmatch;
98//!
99//! # fn example_b() -> Result<(), String> {
100//! let root = env!("CARGO_MANIFEST_DIR");
101//! let pattern = "test-files/c-simple/**/[ah]*.txt";
102//!
103//! let builder = globmatch::Builder::new(pattern)
104//!     .case_sensitive(true)
105//!     .build(root)?;
106//!
107//! let paths: Vec<_> = builder
108//!     .into_iter()
109//!     .filter_entry(|p| !globmatch::is_hidden_entry(p))
110//!     .flatten()
111//!     .collect();
112//!
113//! assert_eq!(4, paths.len());
114//! # Ok(())
115//! # }
116//! # example_b().unwrap();
117//! ```
118//!
119//! # Example: Filtering with `.build_glob`.
120//!
121//! The above examples demonstrated how to search for paths using this crate. Two more builder
122//! functions are available for additional matching on the paths yielded by the iterator, e.g.,
123//! to further limit the files (e.g., based on a global blacklist).
124//!
125//! - [`Builder::build_glob`] to create a single [`Glob`] (caution: the builder only checks
126//!    that the pattern is not empty, but allows absolute paths).
127//! - [`Builder::build_glob_set`] to create a [`Glob`] matcher that contains two globs
128//!   `[glob, **/glob]` out of the specified `glob` parameter of [`Builder::new`]. The pattern
129//!    must not be an absolute path.
130//!
131//! ```
132//! use globmatch;
133//!
134//! # fn example_c() -> Result<(), String> {
135//! let root = env!("CARGO_MANIFEST_DIR");
136//! let pattern = "test-files/c-simple/**/a*.*";
137//!
138//! let builder = globmatch::Builder::new(pattern)
139//!     .case_sensitive(true)
140//!     .build(root)?;
141//!
142//! let glob = globmatch::Builder::new("*.txt").build_glob_set()?;
143//!
144//! let paths: Vec<_> = builder
145//!     .into_iter()
146//!     .filter_entry(|p| !globmatch::is_hidden_entry(p))
147//!     .flatten()
148//!     .filter(|p| glob.is_match(p))
149//!     .collect();
150//!
151//! assert_eq!(4, paths.len());
152//! # Ok(())
153//! # }
154//! # example_c().unwrap();
155//! ```
156//!
157//! [globset]: https://docs.rs/globset
158//! [walkdir]: https://docs.rs/walkdir
159//! [filter_entry]: #IterFilter::filter_entry
160
161#[cfg(doctest)]
162doc_comment::doctest!("../readme.md");
163
164use std::path;
165
166mod error;
167mod iters;
168mod utils;
169
170pub mod wrappers;
171
172pub use crate::error::Error;
173pub use crate::iters::{IterAll, IterFilter};
174pub use crate::utils::{is_hidden_entry, is_hidden_path};
175
176/// Asterisks `*` in a glob do not match path separators (e.g., `/` in unix).
177/// Only a double asterisk `**` match multiple folder levels.
178const REQUIRE_PATHSEP: bool = true;
179
180/// A builder for a matcher or globs.
181///
182/// This builder can be configured to match case sensitive (default) or case insensitive.
183/// A single asterisk will not match path separators, e.g., `*/*.txt` does not match the file
184/// `path/to/file.txt`. Use `**` to match across directory boundaries.
185///
186/// The lifetime `'a` refers to the lifetime of the glob string.
187#[derive(Debug)]
188pub struct Builder<'a> {
189    glob: &'a str,
190    case_sensitive: bool,
191}
192
193impl<'a> Builder<'a> {
194    /// Create a new builder for the given glob.
195    ///
196    /// The glob is not compiled until any of the `build` methods is called.
197    pub fn new(glob: &'a str) -> Builder<'a> {
198        Builder {
199            glob,
200            case_sensitive: true,
201        }
202    }
203
204    /// Toggle whether the glob matches case sensitive or not.
205    ///
206    /// The default setting is to match case **sensitive**.
207    pub fn case_sensitive(&mut self, yes: bool) -> &mut Builder<'a> {
208        self.case_sensitive = yes;
209        self
210    }
211
212    /// The actual facade for `globset::Glob`.
213    #[doc(hidden)]
214    fn glob_for(&self, glob: &str) -> Result<globset::Glob, String> {
215        globset::GlobBuilder::new(glob)
216            .literal_separator(REQUIRE_PATHSEP)
217            .case_insensitive(!self.case_sensitive)
218            .build()
219            .map_err(|err| {
220                format!(
221                    "'{}': {}",
222                    self.glob,
223                    utils::to_upper(err.kind().to_string())
224                )
225            })
226    }
227
228    /// Builds a [`Matcher`] for the given [`Builder`] relative to `root`.
229    ///
230    /// Resolves the relative path prefix for the `glob` that has been provided when creating the
231    /// builder for the given root directory, e.g.,
232    ///
233    /// For the root directory `/path/to/some/folder` and glob `../../*.txt`, this function will
234    /// move the relative path components to the root folder, resulting in only `*.txt` for the
235    /// glob, and `/path/to/some/folder/../../` for the root directory.
236    ///
237    /// Notice that the relative path components will **not** be resolved. The caller of the
238    /// function can map and consolidate each path yielded by the iterator, if required.
239    ///
240    /// # Errors
241    ///
242    /// Simple error messages will be provided in case of failures, e.g., for empty patterns or
243    /// patterns for which the compilation failed; as well as for invalid root directories.
244    pub fn build<P>(&self, root: P) -> Result<Matcher<'a, path::PathBuf>, String>
245    where
246        P: AsRef<path::Path>,
247    {
248        // notice that resolve_root does not return empty patterns
249        let (root, rest) = utils::resolve_root(root, self.glob).map_err(|err| {
250            format!(
251                "'Failed to resolve paths': {}",
252                utils::to_upper(err.to_string())
253            )
254        })?;
255
256        let matcher = self.glob_for(rest)?.compile_matcher();
257        Ok(Matcher {
258            glob: self.glob,
259            root,
260            rest,
261            matcher,
262        })
263    }
264
265    // TODO: allow to build a matcher for absolute paths
266    // meaning, if self.glob is absolute, then simply don't resolve paths
267    // could be a property -> ignore_prefix_if_absolute
268
269    /// Builds a [`Glob`].
270    ///
271    /// This [`Glob`] that can be used for filtering paths provided by a [`Matcher`] (created
272    /// using the `build` function).
273    pub fn build_glob(&self) -> Result<Glob<'a>, String> {
274        if self.glob.is_empty() {
275            return Err("Empty glob".to_string());
276        }
277
278        let matcher = self.glob_for(self.glob)?.compile_matcher();
279        Ok(Glob {
280            glob: self.glob,
281            matcher,
282        })
283    }
284
285    /// Builds a combined [`GlobSet`].
286    ///
287    /// A globset extends the provided `pattern` to `[pattern, **/pattern]`. This is useful, e.g.,
288    /// for blacklists, where only the file type is important.
289    ///
290    /// Yes, it would be sufficient to use the pattern `**/pattern` in the first place. This is
291    /// a simple commodity function.
292    pub fn build_glob_set(&self) -> Result<GlobSet<'a>, String> {
293        if self.glob.is_empty() {
294            return Err("Empty glob".to_string());
295        }
296
297        let p = path::Path::new(self.glob);
298        if p.is_absolute() {
299            return Err(format!("{}' is an absolute path", self.glob));
300        }
301
302        let glob_sub = "**/".to_string() + self.glob;
303
304        let matcher = globset::GlobSetBuilder::new()
305            .add(self.glob_for(self.glob)?)
306            .add(self.glob_for(&glob_sub)?)
307            .build()
308            .map_err(|err| {
309                format!(
310                    "'{}': {}",
311                    self.glob,
312                    utils::to_upper(err.kind().to_string())
313                )
314            })?;
315
316        Ok(GlobSet {
317            glob: self.glob,
318            matcher,
319        })
320    }
321}
322
323/// Matcher type for transformation into an iterator.
324///
325/// This type exists such that [`Builder::build`] can return a result type (whereas `into_iter`
326/// cannot). Notice that `iter()` is not implemented due to the use of references.
327#[derive(Debug)]
328pub struct Matcher<'a, P>
329where
330    P: AsRef<path::Path>,
331{
332    glob: &'a str,
333    /// Original glob-pattern
334    root: P,
335    /// Root path of a resolved pattern
336    rest: &'a str,
337    /// Remaining pattern after root has been resolved
338    matcher: globset::GlobMatcher,
339}
340
341impl<'a, P> IntoIterator for Matcher<'a, P>
342where
343    P: AsRef<path::Path>,
344{
345    type Item = Result<path::PathBuf, Error>;
346    type IntoIter = IterAll<P>;
347
348    /// Transform the [`Matcher`] into a recursive directory iterator.
349    fn into_iter(self) -> Self::IntoIter {
350        let walk_root = path::PathBuf::from(self.root.as_ref());
351        IterAll::new(
352            self.root,
353            walkdir::WalkDir::new(walk_root).into_iter(),
354            self.matcher,
355        )
356    }
357}
358
359impl<'a, P> Matcher<'a, P>
360where
361    P: AsRef<path::Path>,
362{
363    /// Provides the original glob-pattern used to create this [`Matcher`].
364    ///
365    /// This is the unchanged glob, i.e., no relative path components have been resolved.
366    pub fn glob(&self) -> &str {
367        self.glob
368    }
369
370    /// Provides the resolved root folder used by the [`Matcher`].
371    ///
372    /// This directory already contains the path components from the original glob. The main
373    /// intention of this function is to for debugging or logging (thus a String).
374    pub fn root(&self) -> String {
375        let path = path::PathBuf::from(self.root.as_ref());
376        String::from(path.to_str().unwrap())
377    }
378
379    /// Provides the resolved glob used by the [`Matcher`].
380    ///
381    /// All relative path components have been resolved for this glob. The glob is of type &str
382    /// since all globs are input parameters and specified as strings (and not paths).
383    pub fn rest(&self) -> &str {
384        self.rest
385    }
386
387    /// Checks whether the provided path is a match for the stored glob.
388    pub fn is_match(&self, p: P) -> bool {
389        self.matcher.is_match(p)
390    }
391}
392
393/// Wrapper type for glob matching.
394///
395/// This type is created by [`Builder::build_glob`] for a single glob on which no transformations
396/// or path resolutions have been performed.
397#[derive(Debug)]
398pub struct Glob<'a> {
399    glob: &'a str,
400    /// Associated matcher.
401    pub matcher: globset::GlobMatcher,
402}
403
404impl<'a> Glob<'a> {
405    /// Provides the original glob-pattern used to create this [`Glob`].
406    pub fn glob(&self) -> &str {
407        self.glob
408    }
409
410    /// Checks whether the provided path is a match for the stored glob.
411    pub fn is_match<P>(&self, p: P) -> bool
412    where
413        P: AsRef<path::Path>,
414    {
415        self.matcher.is_match(p)
416    }
417}
418
419/// Comfort type for glob matching.
420///
421/// This type is created by [`Builder::build_glob_set`] (refer to the function documentation). The
422/// matcher stores two globs created from the original pattern as `[**/pattern, pattern]` for
423/// easy matching on multiple paths.
424#[derive(Debug)]
425pub struct GlobSet<'a> {
426    glob: &'a str,
427    /// Associated matcher.
428    pub matcher: globset::GlobSet,
429}
430
431impl<'a> GlobSet<'a> {
432    /// Provides the original glob-pattern used to create this [`GlobSet`].
433    pub fn glob(&self) -> &str {
434        self.glob
435    }
436
437    /// Checks whether the provided path is a match for any of the two stored globs.
438    pub fn is_match<P>(&self, p: P) -> bool
439    where
440        P: AsRef<path::Path>,
441    {
442        self.matcher.is_match(p)
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn test_path() {
452        let path = path::Path::new("");
453        assert!(!path.is_absolute());
454    }
455
456    #[test]
457    #[cfg_attr(target_os = "windows", ignore)]
458    fn match_globset() {
459        // yes, it is on purpose that this is a simple list and not read from the test-files/c-simple
460        let files = vec![
461            "/some/path/test-files/c-simple/a",
462            "/some/path/test-files/c-simple/a/a0",
463            "/some/path/test-files/c-simple/a/a0/a0_0.txt",
464            "/some/path/test-files/c-simple/a/a0/a0_1.txt",
465            "/some/path/test-files/c-simple/a/a0/A0_3.txt",
466            "/some/path/test-files/c-simple/a/a0/a0_2.md",
467            "/some/path/test-files/c-simple/a/a1",
468            "/some/path/test-files/c-simple/a/a1/a1_0.txt",
469            "/some/path/test-files/c-simple/a/a2",
470            "/some/path/test-files/c-simple/a/a2/a2_0.txt",
471            "/some/path/test-files/c-simple/b/b_0.txt",
472            "some_file.txt",
473        ];
474
475        // function declaration within function. yay this starts to feel like python :D
476        fn match_glob<'a>(f: &'a str, m: &globset::GlobMatcher) -> Option<&'a str> {
477            match m.is_match(f) {
478                true => Some(f),
479                false => None,
480            }
481        }
482
483        fn glob_for(
484            glob: &str,
485            case_sensitive: bool,
486        ) -> Result<globset::GlobMatcher, globset::Error> {
487            Ok(globset::GlobBuilder::new(glob)
488                .case_insensitive(!case_sensitive)
489                .backslash_escape(true)
490                .literal_separator(REQUIRE_PATHSEP)
491                .build()?
492                .compile_matcher())
493        }
494
495        fn test_for(glob: &str, len: usize, files: &[&str], case_sensitive: bool) {
496            let glob = glob_for(glob, case_sensitive).unwrap();
497            let matches = files
498                .iter()
499                .filter_map(|f| match_glob(f, &glob))
500                .collect::<Vec<_>>();
501            println!(
502                "matches for {}:\n'{}'",
503                glob.glob(),
504                matches
505                    .iter()
506                    .map(|f| f.to_string())
507                    .collect::<Vec<_>>()
508                    .join("\n")
509            );
510            assert_eq!(len, matches.len());
511        }
512
513        test_for("/test-files/c-simple/**/*.txt", 0, &files, true);
514        test_for("test-files/c-simple/**/*.txt", 0, &files, true);
515        test_for("**/test-files/c-simple/**/*.txt", 6, &files, true);
516        test_for("**/test-files/c-simple/**/a*.txt", 4, &files, true);
517        test_for("**/test-files/c-simple/**/a*.txt", 5, &files, false);
518        test_for("**/test-files/c-simple/a/a*/a*.txt", 5, &files, false);
519        test_for("**/test-files/c-simple/a/a[01]/a*.txt", 4, &files, false);
520
521        // this is important, an empty pattern does not match anything
522        test_for("", 0, &files, false);
523
524        // notice that **/*.txt also matches zero recursive levels and thus also "some_file.txt"
525        test_for("**/*.txt", 7, &files, false);
526    }
527
528    #[test]
529    fn builder_build() -> Result<(), String> {
530        let root = env!("CARGO_MANIFEST_DIR");
531        let pattern = "**/*.txt";
532
533        let _builder = Builder::new(pattern).build(root)?;
534        Ok(())
535    }
536
537    #[test]
538    fn builder_err() -> Result<(), String> {
539        let root = env!("CARGO_MANIFEST_DIR");
540        let pattern = "a[";
541
542        match Builder::new(pattern).build(root) {
543            Ok(_) => Err("Expected pattern to fail".to_string()),
544            Err(_) => Ok(()),
545        }
546    }
547
548    #[test]
549    #[cfg(not(target_os = "windows"))]
550    fn match_absolute_pattern() -> Result<(), String> {
551        let root = format!("{}/test-files/c-simple", env!("CARGO_MANIFEST_DIR"));
552        match Builder::new("/test-files/c-simple/**/*.txt").build(root) {
553            Err(_) => Ok(()),
554            Ok(_) => Err("Expected failure".to_string()),
555        }
556    }
557
558    #[test]
559    #[cfg(target_os = "windows")]
560    fn match_absolute_pattern() -> Result<(), String> {
561        let root = format!("{}/test-files/c-simple", env!("CARGO_MANIFEST_DIR"));
562        match Builder::new("C:/test-files/c-simple/**/*.txt").build(root) {
563            Err(_) => Ok(()),
564            Ok(_) => Err("Expected failure".to_string()),
565        }
566    }
567
568    /*
569    some helper functions for testing
570    */
571
572    fn log_paths<P>(paths: &[P])
573    where
574        P: AsRef<path::Path>,
575    {
576        println!(
577            "paths:\n{}",
578            paths
579                .iter()
580                .map(|p| format!("{}", p.as_ref().to_string_lossy()))
581                .collect::<Vec<_>>()
582                .join("\n")
583        );
584    }
585
586    fn log_paths_and_assert<P>(paths: &[P], expected_len: usize)
587    where
588        P: AsRef<path::Path>,
589    {
590        log_paths(paths);
591        assert_eq!(expected_len, paths.len());
592    }
593
594    #[test]
595    fn match_all() -> Result<(), String> {
596        // the following resolves to `<package-root>/test-files/c-simple/**/*.txt` and therefore
597        // successfully matches all files
598        let builder =
599            Builder::new("test-files/c-simple/**/*.txt").build(env!("CARGO_MANIFEST_DIR"))?;
600
601        let paths: Vec<_> = builder.into_iter().flatten().collect();
602        log_paths_and_assert(&paths, 6 + 2 + 1); // this also matches `some_file.txt`
603        Ok(())
604    }
605
606    #[test]
607    fn match_case() -> Result<(), String> {
608        let root = env!("CARGO_MANIFEST_DIR");
609        let pattern = "test-files/c-simple/a/a?/a*.txt";
610
611        // default is case_sensitive(true)
612        let builder = Builder::new(pattern).build(root)?;
613        println!(
614            "working on root {} with glob {:?}",
615            builder.root(),
616            builder.rest()
617        );
618
619        let paths: Vec<_> = builder.into_iter().flatten().collect();
620        log_paths_and_assert(&paths, 4);
621        Ok(())
622    }
623
624    #[test]
625    fn match_filter_entry() -> Result<(), String> {
626        let root = env!("CARGO_MANIFEST_DIR");
627        let pattern = "test-files/c-simple/**/*.txt";
628
629        let builder = Builder::new(pattern).build(root)?;
630        let paths: Vec<_> = builder
631            .into_iter()
632            .filter_entry(|p| !is_hidden_entry(p))
633            .flatten()
634            .collect();
635
636        log_paths_and_assert(&paths, 6 + 1);
637        Ok(())
638    }
639
640    #[test]
641    fn match_filter() -> Result<(), String> {
642        let root = env!("CARGO_MANIFEST_DIR");
643        let pattern = "test-files/c-simple/**/*.txt";
644
645        // this is slower than filter_entry since it matches all hidden paths
646        let builder = Builder::new(pattern).build(root)?;
647        let paths: Vec<_> = builder
648            .into_iter()
649            .flatten()
650            .filter(|p| !is_hidden_path(p))
651            .collect();
652
653        log_paths_and_assert(&paths, 6 + 1);
654        Ok(())
655    }
656
657    #[test]
658    fn match_with_glob() -> Result<(), String> {
659        let root = env!("CARGO_MANIFEST_DIR");
660        let pattern = "test-files/c-simple/**/*.txt";
661
662        let glob = Builder::new("**/test-files/c-simple/a/a[0]/**").build_glob()?;
663        let paths: Vec<_> = Builder::new(pattern)
664            .build(root)?
665            .into_iter()
666            .flatten()
667            .filter(|p| !is_hidden_path(p))
668            .filter(|p| glob.is_match(p))
669            .collect();
670
671        log_paths_and_assert(&paths, 3);
672        Ok(())
673    }
674
675    #[test]
676    fn match_with_glob_all() -> Result<(), String> {
677        let root = env!("CARGO_MANIFEST_DIR");
678        let pattern = "test-files/c-simple/**/*.*";
679
680        // build_glob creates a ["**/pattern", "pattern"] glob such that the user two separate
681        // patterns when scanning for files, e.g., using "*.txt" (which would need "**/*.txt"
682        // as well), but also when specifying paths within this glob.
683        let glob = Builder::new("*.txt").build_glob_set()?;
684        let paths: Vec<_> = Builder::new(pattern)
685            .build(root)?
686            .into_iter()
687            .filter_entry(|e| !is_hidden_entry(e))
688            .flatten()
689            .filter(|p| {
690                let is_match = glob.is_match(p);
691                println!("is match: {p:?} - {is_match}");
692                is_match
693            })
694            .collect();
695
696        log_paths_and_assert(&paths, 6 + 1);
697        Ok(())
698    }
699
700    #[test]
701    fn match_flavours() -> Result<(), String> {
702        // TODO: implememnt tests for different relative pattern styles
703        // TODO: also provide failing tests for relative parts in the rest/remainder glob
704        Ok(())
705    }
706
707    #[test]
708    fn filter_entry_with_glob() -> Result<(), String> {
709        let root = env!("CARGO_MANIFEST_DIR");
710        let pattern = "test-files/c-simple/**/*.txt";
711
712        // the following pattern should match all hidden files and folders
713        let glob = Builder::new(".*").build_glob_set()?;
714
715        let paths: Vec<_> = Builder::new(pattern)
716            .build(root)?
717            .into_iter()
718            .filter_entry(|e| !glob.is_match(e))
719            .flatten()
720            .collect();
721
722        log_paths_and_assert(&paths, 6 + 1);
723        Ok(())
724    }
725}