globwalk/
lib.rs

1// Copyright (c) 2017 Gilad Naaman
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20//! Recursively find files in a directory using globs.
21//!
22//! Features include
23//! - [`gitignore`'s extended glob syntax][gitignore]
24//! - Control over symlink behavior
25//! - Control depth walked
26//! - Control order results are returned
27//!
28//! [gitignore]: https://git-scm.com/docs/gitignore#_pattern_format
29//!
30//! # Examples
31//!
32//! ## Finding image files in the current directory.
33//!
34//! ```rust
35//! extern crate globwalk;
36//! # include!("doctests.rs");
37//!
38//! use std::fs;
39//! # fn run() -> Result<(), Box<dyn ::std::error::Error>> {
40//! # let temp_dir = create_files(&["cow.jog", "cat.gif"])?;
41//! # ::std::env::set_current_dir(&temp_dir)?;
42//!
43//! for img in globwalk::glob("*.{png,jpg,gif}")? {
44//!     if let Ok(img) = img {
45//!         fs::remove_file(img.path())?;
46//!     }
47//! }
48//! # Ok(()) }
49//! # fn main() { run().unwrap() }
50//! ```
51//!
52//! ## Advanced Globbing ###
53//!
54//! By using one of the constructors of `globwalk::GlobWalker`, it is possible to alter the
55//! base-directory or add multiple patterns.
56//!
57//! ```rust
58//! extern crate globwalk;
59//! # include!("doctests.rs");
60//!
61//! use std::fs;
62//!
63//! # fn run() -> Result<(), Box<dyn ::std::error::Error>> {
64//! # let temp_dir = create_files(&["cow.jog", "cat.gif"])?;
65//! # let BASE_DIR = &temp_dir;
66//! let walker = globwalk::GlobWalkerBuilder::from_patterns(
67//!         BASE_DIR,
68//!         &["*.{png,jpg,gif}", "!Pictures/*"],
69//!     )
70//!     .max_depth(4)
71//!     .follow_links(true)
72//!     .build()?
73//!     .into_iter()
74//!     .filter_map(Result::ok);
75//!
76//! for img in walker {
77//!     fs::remove_file(img.path())?;
78//! }
79//! # Ok(()) }
80//! # fn main() { run().unwrap() }
81//! ```
82
83// Our doctests need main to compile; AFAICT this is a false positive generated by clippy
84#![allow(clippy::needless_doctest_main)]
85#![warn(missing_docs)]
86
87use ignore::overrides::{Override, OverrideBuilder};
88use ignore::Match;
89use std::cmp::Ordering;
90use std::path::Path;
91use std::path::PathBuf;
92use walkdir::WalkDir;
93
94/// Error from parsing globs.
95#[derive(Debug)]
96pub struct GlobError(ignore::Error);
97
98/// Error from iterating on files.
99pub type WalkError = walkdir::Error;
100/// A directory entry.
101///
102/// This is the type of value that is yielded from the iterators defined in this crate.
103pub type DirEntry = walkdir::DirEntry;
104
105impl From<std::io::Error> for GlobError {
106    fn from(e: std::io::Error) -> Self {
107        GlobError(e.into())
108    }
109}
110
111impl From<GlobError> for std::io::Error {
112    fn from(e: GlobError) -> Self {
113        if let ignore::Error::Io(e) = e.0 {
114            e
115        } else {
116            std::io::ErrorKind::Other.into()
117        }
118    }
119}
120
121impl std::fmt::Display for GlobError {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
123        self.0.fmt(f)
124    }
125}
126
127impl std::error::Error for GlobError {}
128
129bitflags::bitflags! {
130    /// Possible file type filters.
131    /// Constants can be OR'd to filter for several types at a time.
132    ///
133    /// Note that not all files are represented in this enum.
134    /// For example, a char-device is neither a file, a directory, nor a symlink.
135    pub struct FileType: u32 {
136        #[allow(missing_docs)] const FILE =    0b001;
137        #[allow(missing_docs)] const DIR =     0b010;
138        #[allow(missing_docs)] const SYMLINK = 0b100;
139    }
140}
141
142/// An iterator for recursively yielding glob matches.
143///
144/// The order of elements yielded by this iterator is unspecified.
145pub struct GlobWalkerBuilder {
146    root: PathBuf,
147    patterns: Vec<String>,
148    walker: WalkDir,
149    case_insensitive: bool,
150    file_type: Option<FileType>,
151}
152
153impl GlobWalkerBuilder {
154    /// Construct a new `GlobWalker` with a glob pattern.
155    ///
156    /// When iterated, the `base` directory will be recursively searched for paths
157    /// matching `pattern`.
158    pub fn new<P, S>(base: P, pattern: S) -> Self
159    where
160        P: AsRef<Path>,
161        S: AsRef<str>,
162    {
163        GlobWalkerBuilder::from_patterns(base, &[pattern])
164    }
165
166    /// Construct a new `GlobWalker` from a list of patterns.
167    ///
168    /// When iterated, the `base` directory will be recursively searched for paths
169    /// matching `patterns`.
170    pub fn from_patterns<P, S>(base: P, patterns: &[S]) -> Self
171    where
172        P: AsRef<Path>,
173        S: AsRef<str>,
174    {
175        fn normalize_pattern<S: AsRef<str>>(pattern: S) -> String {
176            // Either `ignore` or our iteration code treat a single asterisk pretty strangely, matching everything, even
177            // paths that are inside a sub-direcrtory.
178            if pattern.as_ref() == "*" {
179                String::from("/*")
180            } else {
181                pattern.as_ref().to_owned()
182            }
183        }
184        GlobWalkerBuilder {
185            root: base.as_ref().into(),
186            patterns: patterns.iter().map(normalize_pattern).collect::<_>(),
187            walker: WalkDir::new(base),
188            case_insensitive: false,
189            file_type: None,
190        }
191    }
192
193    /// Set the minimum depth of entries yielded by the iterator.
194    ///
195    /// The smallest depth is `0` and always corresponds to the path given
196    /// to the `new` function on this type. Its direct descendents have depth
197    /// `1`, and their descendents have depth `2`, and so on.
198    pub fn min_depth(mut self, depth: usize) -> Self {
199        self.walker = self.walker.min_depth(depth);
200        self
201    }
202
203    /// Set the maximum depth of entries yield by the iterator.
204    ///
205    /// The smallest depth is `0` and always corresponds to the path given
206    /// to the `new` function on this type. Its direct descendents have depth
207    /// `1`, and their descendents have depth `2`, and so on.
208    ///
209    /// Note that this will not simply filter the entries of the iterator, but
210    /// it will actually avoid descending into directories when the depth is
211    /// exceeded.
212    pub fn max_depth(mut self, depth: usize) -> Self {
213        self.walker = self.walker.max_depth(depth);
214        self
215    }
216
217    /// Follow symbolic links. By default, this is disabled.
218    ///
219    /// When `yes` is `true`, symbolic links are followed as if they were
220    /// normal directories and files. If a symbolic link is broken or is
221    /// involved in a loop, an error is yielded.
222    ///
223    /// When enabled, the yielded [`DirEntry`] values represent the target of
224    /// the link while the path corresponds to the link. See the [`DirEntry`]
225    /// type for more details.
226    ///
227    /// [`DirEntry`]: struct.DirEntry.html
228    pub fn follow_links(mut self, yes: bool) -> Self {
229        self.walker = self.walker.follow_links(yes);
230        self
231    }
232
233    /// Set the maximum number of simultaneously open file descriptors used
234    /// by the iterator.
235    ///
236    /// `n` must be greater than or equal to `1`. If `n` is `0`, then it is set
237    /// to `1` automatically. If this is not set, then it defaults to some
238    /// reasonably low number.
239    ///
240    /// This setting has no impact on the results yielded by the iterator
241    /// (even when `n` is `1`). Instead, this setting represents a trade off
242    /// between scarce resources (file descriptors) and memory. Namely, when
243    /// the maximum number of file descriptors is reached and a new directory
244    /// needs to be opened to continue iteration, then a previous directory
245    /// handle is closed and has its unyielded entries stored in memory. In
246    /// practice, this is a satisfying trade off because it scales with respect
247    /// to the *depth* of your file tree. Therefore, low values (even `1`) are
248    /// acceptable.
249    ///
250    /// Note that this value does not impact the number of system calls made by
251    /// an exhausted iterator.
252    ///
253    /// # Platform behavior
254    ///
255    /// On Windows, if `follow_links` is enabled, then this limit is not
256    /// respected. In particular, the maximum number of file descriptors opened
257    /// is proportional to the depth of the directory tree traversed.
258    pub fn max_open(mut self, n: usize) -> Self {
259        self.walker = self.walker.max_open(n);
260        self
261    }
262
263    /// Set a function for sorting directory entries.
264    ///
265    /// If a compare function is set, the resulting iterator will return all
266    /// paths in sorted order. The compare function will be called to compare
267    /// entries from the same directory.
268    pub fn sort_by<F>(mut self, cmp: F) -> Self
269    where
270        F: FnMut(&DirEntry, &DirEntry) -> Ordering + Send + Sync + 'static,
271    {
272        self.walker = self.walker.sort_by(cmp);
273        self
274    }
275
276    /// Yield a directory's contents before the directory itself. By default,
277    /// this is disabled.
278    ///
279    /// When `yes` is `false` (as is the default), the directory is yielded
280    /// before its contents are read. This is useful when, e.g. you want to
281    /// skip processing of some directories.
282    ///
283    /// When `yes` is `true`, the iterator yields the contents of a directory
284    /// before yielding the directory itself. This is useful when, e.g. you
285    /// want to recursively delete a directory.
286    pub fn contents_first(mut self, yes: bool) -> Self {
287        self.walker = self.walker.contents_first(yes);
288        self
289    }
290
291    /// Toggle whether the globs should be matched case insensitively or not.
292    ///
293    /// This is disabled by default.
294    pub fn case_insensitive(mut self, yes: bool) -> Self {
295        self.case_insensitive = yes;
296        self
297    }
298
299    /// Toggle filtering by file type.
300    /// `FileType` can be an OR of several types.
301    ///
302    /// Note that not all file-types can be whitelisted by this filter (e.g. char-devices, fifos, etc.)
303    pub fn file_type(mut self, file_type: FileType) -> Self {
304        self.file_type = Some(file_type);
305        self
306    }
307
308    /// Finalize and build a `GlobWalker` instance.
309    pub fn build(self) -> Result<GlobWalker, GlobError> {
310        let mut builder = OverrideBuilder::new(self.root);
311
312        builder
313            .case_insensitive(self.case_insensitive)
314            .map_err(GlobError)?;
315
316        for pattern in self.patterns {
317            builder.add(pattern.as_ref()).map_err(GlobError)?;
318        }
319
320        Ok(GlobWalker {
321            ignore: builder.build().map_err(GlobError)?,
322            walker: self.walker.into_iter(),
323            file_type_filter: self.file_type,
324        })
325    }
326}
327
328/// An iterator which emits glob-matched patterns.
329///
330/// An instance of this type must be constructed through `GlobWalker`,
331/// which uses a builder-style pattern.
332///
333/// The order of the yielded paths is undefined, unless specified by the user
334/// using `GlobWalker::sort_by`.
335pub struct GlobWalker {
336    ignore: Override,
337    walker: walkdir::IntoIter,
338    file_type_filter: Option<FileType>,
339}
340
341impl Iterator for GlobWalker {
342    type Item = Result<DirEntry, WalkError>;
343
344    // Possible optimization - Do not descend into directory that will never be a match
345    fn next(&mut self) -> Option<Self::Item> {
346        let mut skip_dir = false;
347
348        // The outer loop allows us to avoid multiple mutable borrows on `self.walker` when
349        // we want to skip.
350        'skipper: loop {
351            if skip_dir {
352                self.walker.skip_current_dir();
353            }
354
355            // The inner loop just advances the iterator until a match is found.
356            for entry in &mut self.walker {
357                match entry {
358                    Ok(e) => {
359                        let is_dir = e.file_type().is_dir();
360
361                        let file_type = if e.file_type().is_dir() {
362                            Some(FileType::DIR)
363                        } else if e.file_type().is_file() {
364                            Some(FileType::FILE)
365                        } else if e.file_type().is_symlink() {
366                            Some(FileType::SYMLINK)
367                        } else {
368                            None
369                        };
370
371                        let file_type_matches = match (self.file_type_filter.as_ref(), file_type) {
372                            (None, _) => true,
373                            (Some(_), None) => false,
374                            (Some(filter), Some(actual)) => filter.contains(actual),
375                        };
376
377                        // Strip the common base directory so that the matcher will be
378                        // able to recognize the file name.
379                        // `unwrap` here is safe, since walkdir returns the files with relation
380                        // to the given base-dir.
381                        let path = e.path().strip_prefix(self.ignore.path()).unwrap();
382
383                        // The path might be empty after stripping if the current base-directory is matched.
384                        if path.as_os_str().is_empty() {
385                            continue 'skipper;
386                        }
387
388                        match self.ignore.matched(path, is_dir) {
389                            Match::Whitelist(_) if file_type_matches => return Some(Ok(e)),
390                            // If the directory is ignored, quit the iterator loop and
391                            // skip-out of this directory.
392                            Match::Ignore(_) if is_dir => {
393                                skip_dir = true;
394                                continue 'skipper;
395                            }
396                            _ => {}
397                        }
398                    }
399                    Err(e) => {
400                        return Some(Err(e));
401                    }
402                }
403            }
404            break;
405        }
406
407        None
408    }
409}
410
411/// Construct a new `GlobWalkerBuilder` with a glob pattern.
412///
413/// When iterated, the current directory will be recursively searched for paths
414/// matching `pattern`, unless the pattern specifies an absolute path.
415pub fn glob_builder<S: AsRef<str>>(pattern: S) -> GlobWalkerBuilder {
416    // Check to see if the pattern starts with an absolute path
417    let path_pattern: PathBuf = pattern.as_ref().into();
418    if path_pattern.is_absolute() {
419        // If the pattern is an absolute path, split it into the longest base and a pattern.
420        let mut base = PathBuf::new();
421        let mut pattern = PathBuf::new();
422        let mut globbing = false;
423
424        // All `to_str().unwrap()` calls should be valid since the input is a string.
425        for c in path_pattern.components() {
426            let os = c.as_os_str().to_str().unwrap();
427            for c in &["*", "{", "}"][..] {
428                if os.contains(c) {
429                    globbing = true;
430                    break;
431                }
432            }
433
434            if globbing {
435                pattern.push(c);
436            } else {
437                base.push(c);
438            }
439        }
440
441        let pat = pattern.to_str().unwrap();
442        if cfg!(windows) {
443            GlobWalkerBuilder::new(base.to_str().unwrap(), pat.replace('\\', "/"))
444        } else {
445            GlobWalkerBuilder::new(base.to_str().unwrap(), pat)
446        }
447    } else {
448        // If the pattern is relative, start searching from the current directory.
449        GlobWalkerBuilder::new(".", pattern)
450    }
451}
452
453/// Construct a new `GlobWalker` with a glob pattern.
454///
455/// When iterated, the current directory will be recursively searched for paths
456/// matching `pattern`, unless the pattern specifies an absolute path.
457pub fn glob<S: AsRef<str>>(pattern: S) -> Result<GlobWalker, GlobError> {
458    glob_builder(pattern).build()
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use std::fs::{create_dir_all, File};
465    use tempfile::TempDir;
466
467    fn touch(dir: &TempDir, names: &[&str]) {
468        for name in names {
469            let name = normalize_path_sep(name);
470            File::create(dir.path().join(name)).expect("Failed to create a test file");
471        }
472    }
473
474    fn normalize_path_sep<S: AsRef<str>>(s: S) -> String {
475        s.as_ref()
476            .replace("[/]", if cfg!(windows) { "\\" } else { "/" })
477    }
478
479    fn equate_to_expected(g: GlobWalker, mut expected: Vec<String>, dir_path: &Path) {
480        for matched_file in g.into_iter().filter_map(Result::ok) {
481            let path = matched_file
482                .path()
483                .strip_prefix(dir_path)
484                .unwrap()
485                .to_str()
486                .unwrap();
487            let path = normalize_path_sep(path);
488
489            let del_idx = if let Some(idx) = expected.iter().position(|n| &path == n) {
490                idx
491            } else {
492                panic!("Iterated file is unexpected: {}", path);
493            };
494
495            expected.remove(del_idx);
496        }
497
498        // Not equating `.len() == 0` so that the assertion output
499        // will contain the extra files
500        let empty: &[&str] = &[][..];
501        assert_eq!(expected, empty);
502    }
503
504    #[test]
505    fn test_absolute_path() {
506        let dir = TempDir::new().expect("Failed to create temporary folder");
507        let dir_path = dir.path().canonicalize().unwrap();
508
509        touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
510
511        let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
512        let mut cwd = dir_path.clone();
513        cwd.push("*.{png,jpg,gif}");
514
515        let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
516        equate_to_expected(glob, expected, &dir_path);
517    }
518
519    #[test]
520    fn test_new() {
521        let dir = TempDir::new().expect("Failed to create temporary folder");
522        let dir_path = dir.path();
523
524        touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
525
526        let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
527
528        let g = GlobWalkerBuilder::new(dir_path, "*.{png,jpg,gif}")
529            .build()
530            .unwrap();
531
532        equate_to_expected(g, expected, dir_path);
533    }
534
535    #[test]
536    fn test_from_patterns() {
537        let dir = TempDir::new().expect("Failed to create temporary folder");
538        let dir_path = dir.path();
539        create_dir_all(dir_path.join("src/some_mod")).expect("");
540        create_dir_all(dir_path.join("tests")).expect("");
541        create_dir_all(dir_path.join("contrib")).expect("");
542
543        touch(
544            &dir,
545            &[
546                "a.rs",
547                "b.rs",
548                "avocado.rs",
549                "lib.c",
550                "src[/]hello.rs",
551                "src[/]world.rs",
552                "src[/]some_mod[/]unexpected.rs",
553                "src[/]cruel.txt",
554                "contrib[/]README.md",
555                "contrib[/]README.rst",
556                "contrib[/]lib.rs",
557            ][..],
558        );
559
560        let expected: Vec<_> = [
561            "src[/]some_mod[/]unexpected.rs",
562            "src[/]world.rs",
563            "src[/]hello.rs",
564            "lib.c",
565            "contrib[/]lib.rs",
566            "contrib[/]README.md",
567            "contrib[/]README.rst",
568        ]
569        .iter()
570        .map(normalize_path_sep)
571        .collect();
572
573        let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
574        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
575            .build()
576            .unwrap();
577
578        equate_to_expected(glob, expected, dir_path);
579    }
580
581    #[test]
582    fn test_case_insensitive_matching() {
583        let dir = TempDir::new().expect("Failed to create temporary folder");
584        let dir_path = dir.path();
585        create_dir_all(dir_path.join("src/some_mod")).expect("");
586        create_dir_all(dir_path.join("tests")).expect("");
587        create_dir_all(dir_path.join("contrib")).expect("");
588
589        touch(
590            &dir,
591            &[
592                "a.rs",
593                "b.rs",
594                "avocado.RS",
595                "lib.c",
596                "src[/]hello.RS",
597                "src[/]world.RS",
598                "src[/]some_mod[/]unexpected.rs",
599                "src[/]cruel.txt",
600                "contrib[/]README.md",
601                "contrib[/]README.rst",
602                "contrib[/]lib.rs",
603            ][..],
604        );
605
606        let expected: Vec<_> = [
607            "src[/]some_mod[/]unexpected.rs",
608            "src[/]hello.RS",
609            "src[/]world.RS",
610            "lib.c",
611            "contrib[/]lib.rs",
612            "contrib[/]README.md",
613            "contrib[/]README.rst",
614        ]
615        .iter()
616        .map(normalize_path_sep)
617        .collect();
618
619        let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
620        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
621            .case_insensitive(true)
622            .build()
623            .unwrap();
624
625        equate_to_expected(glob, expected, dir_path);
626    }
627
628    #[test]
629    fn test_match_dir() {
630        let dir = TempDir::new().expect("Failed to create temporary folder");
631        let dir_path = dir.path();
632        create_dir_all(dir_path.join("mod")).expect("");
633
634        touch(
635            &dir,
636            &[
637                "a.png",
638                "b.png",
639                "c.png",
640                "mod[/]a.png",
641                "mod[/]b.png",
642                "mod[/]c.png",
643            ][..],
644        );
645
646        let expected: Vec<_> = ["mod"].iter().map(normalize_path_sep).collect();
647        let glob = GlobWalkerBuilder::new(dir_path, "mod").build().unwrap();
648
649        equate_to_expected(glob, expected, dir_path);
650    }
651
652    #[test]
653    fn test_blacklist() {
654        let dir = TempDir::new().expect("Failed to create temporary folder");
655        let dir_path = dir.path();
656        create_dir_all(dir_path.join("src/some_mod")).expect("");
657        create_dir_all(dir_path.join("tests")).expect("");
658        create_dir_all(dir_path.join("contrib")).expect("");
659
660        touch(
661            &dir,
662            &[
663                "a.rs",
664                "b.rs",
665                "avocado.rs",
666                "lib.c",
667                "src[/]hello.rs",
668                "src[/]world.rs",
669                "src[/]some_mod[/]unexpected.rs",
670                "src[/]cruel.txt",
671                "contrib[/]README.md",
672                "contrib[/]README.rst",
673                "contrib[/]lib.rs",
674            ][..],
675        );
676
677        let expected: Vec<_> = [
678            "src[/]some_mod[/]unexpected.rs",
679            "src[/]hello.rs",
680            "lib.c",
681            "contrib[/]lib.rs",
682            "contrib[/]README.md",
683            "contrib[/]README.rst",
684        ]
685        .iter()
686        .map(normalize_path_sep)
687        .collect();
688
689        let patterns = [
690            "src/**/*.rs",
691            "*.c",
692            "**/lib.rs",
693            "**/*.{md,rst}",
694            "!world.rs",
695        ];
696
697        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
698            .build()
699            .unwrap();
700
701        equate_to_expected(glob, expected, dir_path);
702    }
703
704    #[test]
705    fn test_blacklist_dir() {
706        let dir = TempDir::new().expect("Failed to create temporary folder");
707        let dir_path = dir.path();
708        create_dir_all(dir_path.join("Pictures")).expect("");
709
710        touch(
711            &dir,
712            &[
713                "a.png",
714                "b.png",
715                "c.png",
716                "Pictures[/]a.png",
717                "Pictures[/]b.png",
718                "Pictures[/]c.png",
719            ][..],
720        );
721
722        let expected: Vec<_> = ["a.png", "b.png", "c.png"]
723            .iter()
724            .map(normalize_path_sep)
725            .collect();
726
727        let patterns = ["*.{png,jpg,gif}", "!Pictures"];
728        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
729            .build()
730            .unwrap();
731
732        equate_to_expected(glob, expected, dir_path);
733    }
734
735    #[test]
736    fn test_glob_with_double_star_pattern() {
737        let dir = TempDir::new().expect("Failed to create temporary folder");
738        let dir_path = dir.path().canonicalize().unwrap();
739
740        touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
741
742        let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
743        let mut cwd = dir_path.clone();
744        cwd.push("**");
745        cwd.push("*.{png,jpg,gif}");
746        let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
747        equate_to_expected(glob, expected, &dir_path);
748    }
749
750    #[test]
751    fn test_glob_single_star() {
752        let dir = TempDir::new().expect("Failed to create temporary folder");
753        let dir_path = dir.path();
754        create_dir_all(dir_path.join("Pictures")).expect("");
755        create_dir_all(dir_path.join("Pictures").join("b")).expect("");
756
757        touch(
758            &dir,
759            &[
760                "a.png",
761                "b.png",
762                "c.png",
763                "Pictures[/]a.png",
764                "Pictures[/]b.png",
765                "Pictures[/]c.png",
766                "Pictures[/]b[/]c.png",
767                "Pictures[/]b[/]c.png",
768                "Pictures[/]b[/]c.png",
769            ][..],
770        );
771
772        let glob = GlobWalkerBuilder::new(dir_path, "*")
773            .sort_by(|a, b| a.path().cmp(b.path()))
774            .build()
775            .unwrap();
776        let expected = ["Pictures", "a.png", "b.png", "c.png"]
777            .iter()
778            .map(ToString::to_string)
779            .collect();
780        equate_to_expected(glob, expected, dir_path);
781    }
782
783    #[test]
784    fn test_file_type() {
785        let dir = TempDir::new().expect("Failed to create temporary folder");
786        let dir_path = dir.path();
787        create_dir_all(dir_path.join("Pictures")).expect("");
788        create_dir_all(dir_path.join("Pictures").join("b")).expect("");
789
790        touch(
791            &dir,
792            &[
793                "a.png",
794                "b.png",
795                "c.png",
796                "Pictures[/]a.png",
797                "Pictures[/]b.png",
798                "Pictures[/]c.png",
799                "Pictures[/]b[/]c.png",
800                "Pictures[/]b[/]c.png",
801                "Pictures[/]b[/]c.png",
802            ][..],
803        );
804
805        let glob = GlobWalkerBuilder::new(dir_path, "*")
806            .sort_by(|a, b| a.path().cmp(b.path()))
807            .file_type(FileType::DIR)
808            .build()
809            .unwrap();
810        let expected = ["Pictures"].iter().map(ToString::to_string).collect();
811        equate_to_expected(glob, expected, dir_path);
812
813        let glob = GlobWalkerBuilder::new(dir_path, "*")
814            .sort_by(|a, b| a.path().cmp(b.path()))
815            .file_type(FileType::FILE)
816            .build()
817            .unwrap();
818        let expected = ["a.png", "b.png", "c.png"]
819            .iter()
820            .map(ToString::to_string)
821            .collect();
822        equate_to_expected(glob, expected, dir_path);
823    }
824}