globwalker/
lib.rs

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