#![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;
#[derive(Debug)]
pub struct GlobError(ignore::Error);
pub type WalkError = walkdir::Error;
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! {
pub struct FileType: u32 {
#[allow(missing_docs)] const FILE = 0b001;
#[allow(missing_docs)] const DIR = 0b010;
#[allow(missing_docs)] const SYMLINK = 0b100;
}
}
pub struct GlobWalkerBuilder {
root: PathBuf,
patterns: Vec<String>,
walker: WalkDir,
case_insensitive: bool,
file_type: Option<FileType>,
}
impl GlobWalkerBuilder {
pub fn new<P, S>(base: P, pattern: S) -> Self
where
P: AsRef<Path>,
S: AsRef<str>,
{
GlobWalkerBuilder::from_patterns(base, &[pattern])
}
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 {
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,
}
}
pub fn min_depth(mut self, depth: usize) -> Self {
self.walker = self.walker.min_depth(depth);
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.walker = self.walker.max_depth(depth);
self
}
pub fn follow_links(mut self, yes: bool) -> Self {
self.walker = self.walker.follow_links(yes);
self
}
pub fn max_open(mut self, n: usize) -> Self {
self.walker = self.walker.max_open(n);
self
}
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
}
pub fn contents_first(mut self, yes: bool) -> Self {
self.walker = self.walker.contents_first(yes);
self
}
pub fn case_insensitive(mut self, yes: bool) -> Self {
self.case_insensitive = yes;
self
}
pub fn file_type(mut self, file_type: FileType) -> Self {
self.file_type = Some(file_type);
self
}
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,
})
}
}
pub struct GlobWalker {
ignore: Override,
walker: walkdir::IntoIter,
file_type_filter: Option<FileType>,
}
impl Iterator for GlobWalker {
type Item = Result<DirEntry, WalkError>;
fn next(&mut self) -> Option<Self::Item> {
let mut skip_dir = false;
'skipper: loop {
if skip_dir {
self.walker.skip_current_dir();
}
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),
};
let path = e
.path()
.strip_prefix(self.ignore.path())
.unwrap()
.to_owned();
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)),
Match::Ignore(_) if is_dir => {
skip_dir = true;
continue 'skipper;
}
_ => {}
}
}
Err(e) => {
return Some(Err(e));
}
}
}
break;
}
None
}
}
pub fn glob_builder<S: AsRef<str>>(pattern: S) -> GlobWalkerBuilder {
let path_pattern: PathBuf = pattern.as_ref().into();
if path_pattern.is_absolute() {
let mut base = PathBuf::new();
let mut pattern = PathBuf::new();
let mut globbing = false;
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 {
GlobWalkerBuilder::new(".", pattern)
}
}
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);
}
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);
}
}