#![warn(missing_docs)]
extern crate ignore;
extern crate walkdir;
#[cfg(test)]
extern crate tempdir;
use ignore::overrides::{Override, OverrideBuilder};
use ignore::Match;
use std::cmp::Ordering;
use std::path::Path;
use walkdir::WalkDir;
use std::path::PathBuf;
#[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()
}
}
pub struct GlobWalkerBuilder {
root: PathBuf,
patterns: Vec<String>,
walker: WalkDir,
case_insensitive: bool,
}
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>,
{
GlobWalkerBuilder {
root: base.as_ref().into(),
patterns: patterns.iter().map(|s| s.as_ref().to_owned()).collect::<_>(),
walker: WalkDir::new(base),
case_insensitive: false,
}
}
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 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(),
})
}
}
pub struct GlobWalker {
ignore: Override,
walker: walkdir::IntoIter,
}
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();
match self.ignore
.matched(e.path().strip_prefix(self.ignore.path()).unwrap(), is_dir)
{
Match::Whitelist(_) => 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<S: AsRef<str>>(pattern: S) -> Result<GlobWalker, GlobError> {
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("\\", "/")).build()
} else {
GlobWalkerBuilder::new(base.to_str().unwrap(), pat).build()
}
}
else {
GlobWalkerBuilder::new(".", 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 { "/" })
}
#[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 mut expected = vec!["a.jpg", "a.png"];
let mut cwd = dir_path.clone();
cwd.push("*.{png,jpg,gif}");
for matched_file in glob(cwd.to_str().unwrap().to_owned())
.unwrap().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_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 mut expected = vec!["a.jpg", "a.png"];
for matched_file in GlobWalkerBuilder::new(dir_path, "*.{png,jpg,gif}")
.build().unwrap()
.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_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 mut 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}"];
for matched_file in GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.build().unwrap()
.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_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 mut 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}"];
for matched_file in GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.case_insensitive(true).build().unwrap()
.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_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 mut expected: Vec<_> = ["mod"].iter().map(normalize_path_sep).collect();
for matched_file in GlobWalkerBuilder::new(dir_path, "mod")
.build().unwrap()
.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_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 mut 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",
];
for matched_file in GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.build().unwrap()
.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_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 mut expected: Vec<_> = ["a.png", "b.png", "c.png"]
.iter()
.map(normalize_path_sep)
.collect();
let patterns = ["*.{png,jpg,gif}", "!Pictures"];
for matched_file in GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.build().unwrap()
.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_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 mut expected = vec!["a.jpg", "a.png"];
let mut cwd = dir_path.clone();
cwd.push("**");
cwd.push("*.{png,jpg,gif}");
for matched_file in glob(cwd.to_str().unwrap().to_owned())
.unwrap().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);
}
}