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}