globwalk 0.8.1

Glob-matched recursive file system walking.
Documentation
// Copyright (c) 2017 Gilad Naaman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Recursively find files in a directory using globs.
//!
//! Features include
//! - [`gitignore`'s extended glob syntax][gitignore]
//! - Control over symlink behavior
//! - Control depth walked
//! - Control order results are returned
//!
//! [gitignore]: https://git-scm.com/docs/gitignore#_pattern_format
//!
//! # Examples
//!
//! ## Finding image files in the current directory.
//!
//! ```rust
//! extern crate globwalk;
//! # include!("doctests.rs");
//!
//! use std::fs;
//! # fn run() -> Result<(), Box<::std::error::Error>> {
//! # let temp_dir = create_files(&["cow.jog", "cat.gif"])?;
//! # ::std::env::set_current_dir(&temp_dir)?;
//!
//! for img in globwalk::glob("*.{png,jpg,gif}")? {
//!     if let Ok(img) = img {
//!         fs::remove_file(img.path())?;
//!     }
//! }
//! # Ok(()) }
//! # fn main() { run().unwrap() }
//! ```
//!
//! ## Advanced Globbing ###
//!
//! By using one of the constructors of `globwalk::GlobWalker`, it is possible to alter the
//! base-directory or add multiple patterns.
//!
//! ```rust
//! extern crate globwalk;
//! # include!("doctests.rs");
//!
//! use std::fs;
//!
//! # fn run() -> Result<(), Box<::std::error::Error>> {
//! # let temp_dir = create_files(&["cow.jog", "cat.gif"])?;
//! # let BASE_DIR = &temp_dir;
//! let walker = globwalk::GlobWalkerBuilder::from_patterns(
//!         BASE_DIR,
//!         &["*.{png,jpg,gif}", "!Pictures/*"],
//!     )
//!     .max_depth(4)
//!     .follow_links(true)
//!     .build()?
//!     .into_iter()
//!     .filter_map(Result::ok);
//!
//! for img in walker {
//!     fs::remove_file(img.path())?;
//! }
//! # Ok(()) }
//! # fn main() { run().unwrap() }
//! ```

// Our doctests need main to compile; AFAICT this is a false positive generated by clippy
#![allow(clippy::needless_doctest_main)]
#![warn(missing_docs)]

extern crate ignore;
extern crate walkdir;

extern crate bitflags;
#[cfg(test)]
extern crate tempdir;

use ignore::overrides::{Override, OverrideBuilder};
use ignore::Match;
use std::cmp::Ordering;
use std::path::Path;
use std::path::PathBuf;
use walkdir::WalkDir;

/// Error from parsing globs.
#[derive(Debug)]
pub struct GlobError(ignore::Error);

/// Error from iterating on files.
pub type WalkError = walkdir::Error;
/// A directory entry.
///
/// This is the type of value that is yielded from the iterators defined in this crate.
pub type DirEntry = walkdir::DirEntry;

impl From<std::io::Error> for GlobError {
    fn from(e: std::io::Error) -> Self {
        GlobError(e.into())
    }
}

impl From<GlobError> for std::io::Error {
    fn from(e: GlobError) -> Self {
        if let ignore::Error::Io(e) = e.0 {
            e
        } else {
            std::io::ErrorKind::Other.into()
        }
    }
}

impl std::fmt::Display for GlobError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        self.0.fmt(f)
    }
}

impl std::error::Error for GlobError {
    fn description(&self) -> &str {
        self.0.description()
    }
}

bitflags::bitflags! {
    /// Possible file type filters.
    /// Constants can be OR'd to filter for several types at a time.
    ///
    /// Note that not all files are represented in this enum.
    /// For example, a char-device is neither a file, a directory, nor a symlink.
    pub struct FileType: u32 {
        #[allow(missing_docs)] const FILE =    0b001;
        #[allow(missing_docs)] const DIR =     0b010;
        #[allow(missing_docs)] const SYMLINK = 0b100;
    }
}

/// An iterator for recursively yielding glob matches.
///
/// The order of elements yielded by this iterator is unspecified.
pub struct GlobWalkerBuilder {
    root: PathBuf,
    patterns: Vec<String>,
    walker: WalkDir,
    case_insensitive: bool,
    file_type: Option<FileType>,
}

impl GlobWalkerBuilder {
    /// Construct a new `GlobWalker` with a glob pattern.
    ///
    /// When iterated, the `base` directory will be recursively searched for paths
    /// matching `pattern`.
    pub fn new<P, S>(base: P, pattern: S) -> Self
    where
        P: AsRef<Path>,
        S: AsRef<str>,
    {
        GlobWalkerBuilder::from_patterns(base, &[pattern])
    }

    /// Construct a new `GlobWalker` from a list of patterns.
    ///
    /// When iterated, the `base` directory will be recursively searched for paths
    /// matching `patterns`.
    pub fn from_patterns<P, S>(base: P, patterns: &[S]) -> Self
    where
        P: AsRef<Path>,
        S: AsRef<str>,
    {
        fn normalize_pattern<S: AsRef<str>>(pattern: S) -> String {
            // Either `ignore` or our iteration code treat a single asterisk pretty strangely, matching everything, even
            // paths that are inside a sub-direcrtory.
            if pattern.as_ref() == "*" {
                String::from("/*")
            } else {
                pattern.as_ref().to_owned()
            }
        }
        GlobWalkerBuilder {
            root: base.as_ref().into(),
            patterns: patterns.iter().map(normalize_pattern).collect::<_>(),
            walker: WalkDir::new(base),
            case_insensitive: false,
            file_type: None,
        }
    }

    /// Set the minimum depth of entries yielded by the iterator.
    ///
    /// The smallest depth is `0` and always corresponds to the path given
    /// to the `new` function on this type. Its direct descendents have depth
    /// `1`, and their descendents have depth `2`, and so on.
    pub fn min_depth(mut self, depth: usize) -> Self {
        self.walker = self.walker.min_depth(depth);
        self
    }

    /// Set the maximum depth of entries yield by the iterator.
    ///
    /// The smallest depth is `0` and always corresponds to the path given
    /// to the `new` function on this type. Its direct descendents have depth
    /// `1`, and their descendents have depth `2`, and so on.
    ///
    /// Note that this will not simply filter the entries of the iterator, but
    /// it will actually avoid descending into directories when the depth is
    /// exceeded.
    pub fn max_depth(mut self, depth: usize) -> Self {
        self.walker = self.walker.max_depth(depth);
        self
    }

    /// Follow symbolic links. By default, this is disabled.
    ///
    /// When `yes` is `true`, symbolic links are followed as if they were
    /// normal directories and files. If a symbolic link is broken or is
    /// involved in a loop, an error is yielded.
    ///
    /// When enabled, the yielded [`DirEntry`] values represent the target of
    /// the link while the path corresponds to the link. See the [`DirEntry`]
    /// type for more details.
    ///
    /// [`DirEntry`]: struct.DirEntry.html
    pub fn follow_links(mut self, yes: bool) -> Self {
        self.walker = self.walker.follow_links(yes);
        self
    }

    /// Set the maximum number of simultaneously open file descriptors used
    /// by the iterator.
    ///
    /// `n` must be greater than or equal to `1`. If `n` is `0`, then it is set
    /// to `1` automatically. If this is not set, then it defaults to some
    /// reasonably low number.
    ///
    /// This setting has no impact on the results yielded by the iterator
    /// (even when `n` is `1`). Instead, this setting represents a trade off
    /// between scarce resources (file descriptors) and memory. Namely, when
    /// the maximum number of file descriptors is reached and a new directory
    /// needs to be opened to continue iteration, then a previous directory
    /// handle is closed and has its unyielded entries stored in memory. In
    /// practice, this is a satisfying trade off because it scales with respect
    /// to the *depth* of your file tree. Therefore, low values (even `1`) are
    /// acceptable.
    ///
    /// Note that this value does not impact the number of system calls made by
    /// an exhausted iterator.
    ///
    /// # Platform behavior
    ///
    /// On Windows, if `follow_links` is enabled, then this limit is not
    /// respected. In particular, the maximum number of file descriptors opened
    /// is proportional to the depth of the directory tree traversed.
    pub fn max_open(mut self, n: usize) -> Self {
        self.walker = self.walker.max_open(n);
        self
    }

    /// Set a function for sorting directory entries.
    ///
    /// If a compare function is set, the resulting iterator will return all
    /// paths in sorted order. The compare function will be called to compare
    /// entries from the same directory.
    pub fn sort_by<F>(mut self, cmp: F) -> Self
    where
        F: FnMut(&DirEntry, &DirEntry) -> Ordering + Send + Sync + 'static,
    {
        self.walker = self.walker.sort_by(cmp);
        self
    }

    /// Yield a directory's contents before the directory itself. By default,
    /// this is disabled.
    ///
    /// When `yes` is `false` (as is the default), the directory is yielded
    /// before its contents are read. This is useful when, e.g. you want to
    /// skip processing of some directories.
    ///
    /// When `yes` is `true`, the iterator yields the contents of a directory
    /// before yielding the directory itself. This is useful when, e.g. you
    /// want to recursively delete a directory.
    pub fn contents_first(mut self, yes: bool) -> Self {
        self.walker = self.walker.contents_first(yes);
        self
    }

    /// Toggle whether the globs should be matched case insensitively or not.
    ///
    /// This is disabled by default.
    pub fn case_insensitive(mut self, yes: bool) -> Self {
        self.case_insensitive = yes;
        self
    }

    /// Toggle filtering by file type.
    /// `FileType` can be an OR of several types.
    ///
    /// Note that not all file-types can be whitelisted by this filter (e.g. char-devices, fifos, etc.)
    pub fn file_type(mut self, file_type: FileType) -> Self {
        self.file_type = Some(file_type);
        self
    }

    /// Finalize and build a `GlobWalker` instance.
    pub fn build(self) -> Result<GlobWalker, GlobError> {
        let mut builder = OverrideBuilder::new(self.root);

        builder
            .case_insensitive(self.case_insensitive)
            .map_err(GlobError)?;

        for pattern in self.patterns {
            builder.add(pattern.as_ref()).map_err(GlobError)?;
        }

        Ok(GlobWalker {
            ignore: builder.build().map_err(GlobError)?,
            walker: self.walker.into_iter(),
            file_type_filter: self.file_type,
        })
    }
}

/// An iterator which emits glob-matched patterns.
///
/// An instance of this type must be constructed through `GlobWalker`,
/// which uses a builder-style pattern.
///
/// The order of the yielded paths is undefined, unless specified by the user
/// using `GlobWalker::sort_by`.
pub struct GlobWalker {
    ignore: Override,
    walker: walkdir::IntoIter,
    file_type_filter: Option<FileType>,
}

impl Iterator for GlobWalker {
    type Item = Result<DirEntry, WalkError>;

    // Possible optimization - Do not descend into directory that will never be a match
    fn next(&mut self) -> Option<Self::Item> {
        let mut skip_dir = false;

        // The outer loop allows us to avoid multiple mutable borrows on `self.walker` when
        // we want to skip.
        'skipper: loop {
            if skip_dir {
                self.walker.skip_current_dir();
            }

            // The inner loop just advances the iterator until a match is found.
            for entry in &mut self.walker {
                match entry {
                    Ok(e) => {
                        let is_dir = e.file_type().is_dir();

                        let file_type = if e.file_type().is_dir() {
                            Some(FileType::DIR)
                        } else if e.file_type().is_file() {
                            Some(FileType::FILE)
                        } else if e.file_type().is_symlink() {
                            Some(FileType::SYMLINK)
                        } else {
                            None
                        };

                        let file_type_matches = match (self.file_type_filter, file_type) {
                            (None, _) => true,
                            (Some(_), None) => false,
                            (Some(filter), Some(actual)) => filter.contains(actual),
                        };

                        // Strip the common base directory so that the matcher will be
                        // able to recognize the file name.
                        // `unwrap` here is safe, since walkdir returns the files with relation
                        // to the given base-dir.
                        let path = e
                            .path()
                            .strip_prefix(self.ignore.path())
                            .unwrap()
                            .to_owned();

                        // The path might be empty after stripping if the current base-directory is matched.
                        if let Some("") = path.to_str() {
                            continue 'skipper;
                        }

                        match self.ignore.matched(path, is_dir) {
                            Match::Whitelist(_) if file_type_matches => return Some(Ok(e)),
                            // If the directory is ignored, quit the iterator loop and
                            // skip-out of this directory.
                            Match::Ignore(_) if is_dir => {
                                skip_dir = true;
                                continue 'skipper;
                            }
                            _ => {}
                        }
                    }
                    Err(e) => {
                        return Some(Err(e));
                    }
                }
            }
            break;
        }

        None
    }
}

/// Construct a new `GlobWalkerBuilder` with a glob pattern.
///
/// When iterated, the current directory will be recursively searched for paths
/// matching `pattern`, unless the pattern specifies an absolute path.
pub fn glob_builder<S: AsRef<str>>(pattern: S) -> GlobWalkerBuilder {
    // Check to see if the pattern starts with an absolute path
    let path_pattern: PathBuf = pattern.as_ref().into();
    if path_pattern.is_absolute() {
        // If the pattern is an absolute path, split it into the longest base and a pattern.
        let mut base = PathBuf::new();
        let mut pattern = PathBuf::new();
        let mut globbing = false;

        // All `to_str().unwrap()` calls should be valid since the input is a string.
        for c in path_pattern.components() {
            let os = c.as_os_str().to_str().unwrap();
            for c in &["*", "{", "}"][..] {
                if os.contains(c) {
                    globbing = true;
                    break;
                }
            }

            if globbing {
                pattern.push(c);
            } else {
                base.push(c);
            }
        }

        let pat = pattern.to_str().unwrap();
        if cfg!(windows) {
            GlobWalkerBuilder::new(base.to_str().unwrap(), pat.replace("\\", "/"))
        } else {
            GlobWalkerBuilder::new(base.to_str().unwrap(), pat)
        }
    } else {
        // If the pattern is relative, start searching from the current directory.
        GlobWalkerBuilder::new(".", pattern)
    }
}

/// Construct a new `GlobWalker` with a glob pattern.
///
/// When iterated, the current directory will be recursively searched for paths
/// matching `pattern`, unless the pattern specifies an absolute path.
pub fn glob<S: AsRef<str>>(pattern: S) -> Result<GlobWalker, GlobError> {
    glob_builder(pattern).build()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::{create_dir_all, File};
    use tempdir::TempDir;

    fn touch(dir: &TempDir, names: &[&str]) {
        for name in names {
            let name = normalize_path_sep(name);
            File::create(dir.path().join(name)).expect("Failed to create a test file");
        }
    }

    fn normalize_path_sep<S: AsRef<str>>(s: S) -> String {
        s.as_ref()
            .replace("[/]", if cfg!(windows) { "\\" } else { "/" })
    }

    fn equate_to_expected(g: GlobWalker, mut expected: Vec<String>, dir_path: &Path) {
        for matched_file in g.into_iter().filter_map(Result::ok) {
            let path = matched_file
                .path()
                .strip_prefix(dir_path)
                .unwrap()
                .to_str()
                .unwrap();
            let path = normalize_path_sep(path);

            let del_idx = if let Some(idx) = expected.iter().position(|n| &path == n) {
                idx
            } else {
                panic!("Iterated file is unexpected: {}", path);
            };

            expected.remove(del_idx);
        }

        // Not equating `.len() == 0` so that the assertion output
        // will contain the extra files
        let empty: &[&str] = &[][..];
        assert_eq!(expected, empty);
    }

    #[test]
    fn test_absolute_path() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path().canonicalize().unwrap();

        touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);

        let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
        let mut cwd = dir_path.clone();
        cwd.push("*.{png,jpg,gif}");

        let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
        equate_to_expected(glob, expected, &dir_path);
    }

    #[test]
    fn test_new() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();

        touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);

        let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();

        let g = GlobWalkerBuilder::new(dir_path, "*.{png,jpg,gif}")
            .build()
            .unwrap();

        equate_to_expected(g, expected, dir_path);
    }

    #[test]
    fn test_from_patterns() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("src/some_mod")).expect("");
        create_dir_all(dir_path.join("tests")).expect("");
        create_dir_all(dir_path.join("contrib")).expect("");

        touch(
            &dir,
            &[
                "a.rs",
                "b.rs",
                "avocado.rs",
                "lib.c",
                "src[/]hello.rs",
                "src[/]world.rs",
                "src[/]some_mod[/]unexpected.rs",
                "src[/]cruel.txt",
                "contrib[/]README.md",
                "contrib[/]README.rst",
                "contrib[/]lib.rs",
            ][..],
        );

        let expected: Vec<_> = [
            "src[/]some_mod[/]unexpected.rs",
            "src[/]world.rs",
            "src[/]hello.rs",
            "lib.c",
            "contrib[/]lib.rs",
            "contrib[/]README.md",
            "contrib[/]README.rst",
        ]
        .iter()
        .map(normalize_path_sep)
        .collect();

        let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
            .build()
            .unwrap();

        equate_to_expected(glob, expected, dir_path);
    }

    #[test]
    fn test_case_insensitive_matching() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("src/some_mod")).expect("");
        create_dir_all(dir_path.join("tests")).expect("");
        create_dir_all(dir_path.join("contrib")).expect("");

        touch(
            &dir,
            &[
                "a.rs",
                "b.rs",
                "avocado.RS",
                "lib.c",
                "src[/]hello.RS",
                "src[/]world.RS",
                "src[/]some_mod[/]unexpected.rs",
                "src[/]cruel.txt",
                "contrib[/]README.md",
                "contrib[/]README.rst",
                "contrib[/]lib.rs",
            ][..],
        );

        let expected: Vec<_> = [
            "src[/]some_mod[/]unexpected.rs",
            "src[/]hello.RS",
            "src[/]world.RS",
            "lib.c",
            "contrib[/]lib.rs",
            "contrib[/]README.md",
            "contrib[/]README.rst",
        ]
        .iter()
        .map(normalize_path_sep)
        .collect();

        let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
            .case_insensitive(true)
            .build()
            .unwrap();

        equate_to_expected(glob, expected, dir_path);
    }

    #[test]
    fn test_match_dir() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("mod")).expect("");

        touch(
            &dir,
            &[
                "a.png",
                "b.png",
                "c.png",
                "mod[/]a.png",
                "mod[/]b.png",
                "mod[/]c.png",
            ][..],
        );

        let expected: Vec<_> = ["mod"].iter().map(normalize_path_sep).collect();
        let glob = GlobWalkerBuilder::new(dir_path, "mod").build().unwrap();

        equate_to_expected(glob, expected, dir_path);
    }

    #[test]
    fn test_blacklist() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("src/some_mod")).expect("");
        create_dir_all(dir_path.join("tests")).expect("");
        create_dir_all(dir_path.join("contrib")).expect("");

        touch(
            &dir,
            &[
                "a.rs",
                "b.rs",
                "avocado.rs",
                "lib.c",
                "src[/]hello.rs",
                "src[/]world.rs",
                "src[/]some_mod[/]unexpected.rs",
                "src[/]cruel.txt",
                "contrib[/]README.md",
                "contrib[/]README.rst",
                "contrib[/]lib.rs",
            ][..],
        );

        let expected: Vec<_> = [
            "src[/]some_mod[/]unexpected.rs",
            "src[/]hello.rs",
            "lib.c",
            "contrib[/]lib.rs",
            "contrib[/]README.md",
            "contrib[/]README.rst",
        ]
        .iter()
        .map(normalize_path_sep)
        .collect();

        let patterns = [
            "src/**/*.rs",
            "*.c",
            "**/lib.rs",
            "**/*.{md,rst}",
            "!world.rs",
        ];

        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
            .build()
            .unwrap();

        equate_to_expected(glob, expected, dir_path);
    }

    #[test]
    fn test_blacklist_dir() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("Pictures")).expect("");

        touch(
            &dir,
            &[
                "a.png",
                "b.png",
                "c.png",
                "Pictures[/]a.png",
                "Pictures[/]b.png",
                "Pictures[/]c.png",
            ][..],
        );

        let expected: Vec<_> = ["a.png", "b.png", "c.png"]
            .iter()
            .map(normalize_path_sep)
            .collect();

        let patterns = ["*.{png,jpg,gif}", "!Pictures"];
        let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
            .build()
            .unwrap();

        equate_to_expected(glob, expected, dir_path);
    }

    #[test]
    fn test_glob_with_double_star_pattern() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path().canonicalize().unwrap();

        touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);

        let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
        let mut cwd = dir_path.clone();
        cwd.push("**");
        cwd.push("*.{png,jpg,gif}");
        let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
        equate_to_expected(glob, expected, &dir_path);
    }

    #[test]
    fn test_glob_single_star() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("Pictures")).expect("");
        create_dir_all(dir_path.join("Pictures").join("b")).expect("");

        touch(
            &dir,
            &[
                "a.png",
                "b.png",
                "c.png",
                "Pictures[/]a.png",
                "Pictures[/]b.png",
                "Pictures[/]c.png",
                "Pictures[/]b[/]c.png",
                "Pictures[/]b[/]c.png",
                "Pictures[/]b[/]c.png",
            ][..],
        );

        let glob = GlobWalkerBuilder::new(dir_path, "*")
            .sort_by(|a, b| a.path().cmp(b.path()))
            .build()
            .unwrap();
        let expected = ["Pictures", "a.png", "b.png", "c.png"]
            .iter()
            .map(ToString::to_string)
            .collect();
        equate_to_expected(glob, expected, dir_path);
    }

    #[test]
    fn test_file_type() {
        let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
        let dir_path = dir.path();
        create_dir_all(dir_path.join("Pictures")).expect("");
        create_dir_all(dir_path.join("Pictures").join("b")).expect("");

        touch(
            &dir,
            &[
                "a.png",
                "b.png",
                "c.png",
                "Pictures[/]a.png",
                "Pictures[/]b.png",
                "Pictures[/]c.png",
                "Pictures[/]b[/]c.png",
                "Pictures[/]b[/]c.png",
                "Pictures[/]b[/]c.png",
            ][..],
        );

        let glob = GlobWalkerBuilder::new(dir_path, "*")
            .sort_by(|a, b| a.path().cmp(b.path()))
            .file_type(FileType::DIR)
            .build()
            .unwrap();
        let expected = ["Pictures"].iter().map(ToString::to_string).collect();
        equate_to_expected(glob, expected, dir_path);

        let glob = GlobWalkerBuilder::new(dir_path, "*")
            .sort_by(|a, b| a.path().cmp(b.path()))
            .file_type(FileType::FILE)
            .build()
            .unwrap();
        let expected = ["a.png", "b.png", "c.png"]
            .iter()
            .map(ToString::to_string)
            .collect();
        equate_to_expected(glob, expected, dir_path);
    }
}