#![warn(missing_debug_implementations, rust_2018_idioms, missing_docs)]
#[cfg(doctest)]
doc_comment::doctest!("../readme.md");
use std::path;
mod error;
mod iters;
mod utils;
pub mod wrappers;
pub use crate::error::Error;
pub use crate::iters::{IterAll, IterFilter};
pub use crate::utils::{is_hidden_entry, is_hidden_path};
const REQUIRE_PATHSEP: bool = true;
#[derive(Debug)]
pub struct Builder<'a> {
glob: &'a str,
case_sensitive: bool,
}
impl<'a> Builder<'a> {
pub fn new(glob: &'a str) -> Builder<'a> {
Builder {
glob,
case_sensitive: true,
}
}
pub fn case_sensitive(&mut self, yes: bool) -> &mut Builder<'a> {
self.case_sensitive = yes;
self
}
#[doc(hidden)]
fn glob_for(&self, glob: &str) -> Result<globset::Glob, String> {
globset::GlobBuilder::new(glob)
.literal_separator(REQUIRE_PATHSEP)
.case_insensitive(!self.case_sensitive)
.build()
.map_err(|err| {
format!(
"'{}': {}",
self.glob,
utils::to_upper(err.kind().to_string())
)
})
}
pub fn build<P>(&self, root: P) -> Result<Matcher<'a, path::PathBuf>, String>
where
P: AsRef<path::Path>,
{
let (root, rest) = utils::resolve_root(root, self.glob).map_err(|err| {
format!(
"'Failed to resolve paths': {}",
utils::to_upper(err.to_string())
)
})?;
let matcher = self.glob_for(rest)?.compile_matcher();
Ok(Matcher {
glob: self.glob,
root,
rest,
matcher,
})
}
pub fn build_glob(&self) -> Result<Glob<'a>, String> {
if self.glob.is_empty() {
return Err("Empty glob".to_string());
}
let matcher = self.glob_for(self.glob)?.compile_matcher();
Ok(Glob {
glob: self.glob,
matcher,
})
}
pub fn build_glob_set(&self) -> Result<GlobSet<'a>, String> {
if self.glob.is_empty() {
return Err("Empty glob".to_string());
}
let p = path::Path::new(self.glob);
if p.is_absolute() {
return Err(format!("{}' is an absolute path", self.glob));
}
let glob_sub = "**/".to_string() + self.glob;
let matcher = globset::GlobSetBuilder::new()
.add(self.glob_for(self.glob)?)
.add(self.glob_for(&glob_sub)?)
.build()
.map_err(|err| {
format!(
"'{}': {}",
self.glob,
utils::to_upper(err.kind().to_string())
)
})?;
Ok(GlobSet {
glob: self.glob,
matcher,
})
}
}
#[derive(Debug)]
pub struct Matcher<'a, P>
where
P: AsRef<path::Path>,
{
glob: &'a str,
root: P,
rest: &'a str,
matcher: globset::GlobMatcher,
}
impl<'a, P> IntoIterator for Matcher<'a, P>
where
P: AsRef<path::Path>,
{
type Item = Result<path::PathBuf, Error>;
type IntoIter = IterAll<P>;
fn into_iter(self) -> Self::IntoIter {
let walk_root = path::PathBuf::from(self.root.as_ref());
IterAll::new(
self.root,
walkdir::WalkDir::new(walk_root).into_iter(),
self.matcher,
)
}
}
impl<'a, P> Matcher<'a, P>
where
P: AsRef<path::Path>,
{
pub fn glob(&self) -> &str {
self.glob
}
pub fn root(&self) -> String {
let path = path::PathBuf::from(self.root.as_ref());
String::from(path.to_str().unwrap())
}
pub fn rest(&self) -> &str {
self.rest
}
pub fn is_match(&self, p: P) -> bool {
self.matcher.is_match(p)
}
}
#[derive(Debug)]
pub struct Glob<'a> {
glob: &'a str,
pub matcher: globset::GlobMatcher,
}
impl<'a> Glob<'a> {
pub fn glob(&self) -> &str {
self.glob
}
pub fn is_match<P>(&self, p: P) -> bool
where
P: AsRef<path::Path>,
{
self.matcher.is_match(p)
}
}
#[derive(Debug)]
pub struct GlobSet<'a> {
glob: &'a str,
pub matcher: globset::GlobSet,
}
impl<'a> GlobSet<'a> {
pub fn glob(&self) -> &str {
self.glob
}
pub fn is_match<P>(&self, p: P) -> bool
where
P: AsRef<path::Path>,
{
self.matcher.is_match(p)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path() {
let path = path::Path::new("");
assert!(!path.is_absolute());
}
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn match_globset() {
let files = vec![
"/some/path/test-files/c-simple/a",
"/some/path/test-files/c-simple/a/a0",
"/some/path/test-files/c-simple/a/a0/a0_0.txt",
"/some/path/test-files/c-simple/a/a0/a0_1.txt",
"/some/path/test-files/c-simple/a/a0/A0_3.txt",
"/some/path/test-files/c-simple/a/a0/a0_2.md",
"/some/path/test-files/c-simple/a/a1",
"/some/path/test-files/c-simple/a/a1/a1_0.txt",
"/some/path/test-files/c-simple/a/a2",
"/some/path/test-files/c-simple/a/a2/a2_0.txt",
"/some/path/test-files/c-simple/b/b_0.txt",
"some_file.txt",
];
fn match_glob<'a>(f: &'a str, m: &globset::GlobMatcher) -> Option<&'a str> {
match m.is_match(f) {
true => Some(f),
false => None,
}
}
fn glob_for(
glob: &str,
case_sensitive: bool,
) -> Result<globset::GlobMatcher, globset::Error> {
Ok(globset::GlobBuilder::new(glob)
.case_insensitive(!case_sensitive)
.backslash_escape(true)
.literal_separator(REQUIRE_PATHSEP)
.build()?
.compile_matcher())
}
fn test_for(glob: &str, len: usize, files: &[&str], case_sensitive: bool) {
let glob = glob_for(glob, case_sensitive).unwrap();
let matches = files
.iter()
.filter_map(|f| match_glob(f, &glob))
.collect::<Vec<_>>();
println!(
"matches for {}:\n'{}'",
glob.glob(),
matches
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join("\n")
);
assert_eq!(len, matches.len());
}
test_for("/test-files/c-simple/**/*.txt", 0, &files, true);
test_for("test-files/c-simple/**/*.txt", 0, &files, true);
test_for("**/test-files/c-simple/**/*.txt", 6, &files, true);
test_for("**/test-files/c-simple/**/a*.txt", 4, &files, true);
test_for("**/test-files/c-simple/**/a*.txt", 5, &files, false);
test_for("**/test-files/c-simple/a/a*/a*.txt", 5, &files, false);
test_for("**/test-files/c-simple/a/a[01]/a*.txt", 4, &files, false);
test_for("", 0, &files, false);
test_for("**/*.txt", 7, &files, false);
}
#[test]
fn builder_build() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "**/*.txt";
let _builder = Builder::new(pattern).build(root)?;
Ok(())
}
#[test]
fn builder_err() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "a[";
match Builder::new(pattern).build(root) {
Ok(_) => Err("Expected pattern to fail".to_string()),
Err(_) => Ok(()),
}
}
#[test]
#[cfg(not(target_os = "windows"))]
fn match_absolute_pattern() -> Result<(), String> {
let root = format!("{}/test-files/c-simple", env!("CARGO_MANIFEST_DIR"));
match Builder::new("/test-files/c-simple/**/*.txt").build(root) {
Err(_) => Ok(()),
Ok(_) => Err("Expected failure".to_string()),
}
}
#[test]
#[cfg(target_os = "windows")]
fn match_absolute_pattern() -> Result<(), String> {
let root = format!("{}/test-files/c-simple", env!("CARGO_MANIFEST_DIR"));
match Builder::new("C:/test-files/c-simple/**/*.txt").build(root) {
Err(_) => Ok(()),
Ok(_) => Err("Expected failure".to_string()),
}
}
fn log_paths<P>(paths: &[P])
where
P: AsRef<path::Path>,
{
println!(
"paths:\n{}",
paths
.iter()
.map(|p| format!("{}", p.as_ref().to_string_lossy()))
.collect::<Vec<_>>()
.join("\n")
);
}
fn log_paths_and_assert<P>(paths: &[P], expected_len: usize)
where
P: AsRef<path::Path>,
{
log_paths(paths);
assert_eq!(expected_len, paths.len());
}
#[test]
fn match_all() -> Result<(), String> {
let builder =
Builder::new("test-files/c-simple/**/*.txt").build(env!("CARGO_MANIFEST_DIR"))?;
let paths: Vec<_> = builder.into_iter().flatten().collect();
log_paths_and_assert(&paths, 6 + 2 + 1); Ok(())
}
#[test]
fn match_case() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "test-files/c-simple/a/a?/a*.txt";
let builder = Builder::new(pattern).build(root)?;
println!(
"working on root {} with glob {:?}",
builder.root(),
builder.rest()
);
let paths: Vec<_> = builder.into_iter().flatten().collect();
log_paths_and_assert(&paths, 4);
Ok(())
}
#[test]
fn match_filter_entry() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "test-files/c-simple/**/*.txt";
let builder = Builder::new(pattern).build(root)?;
let paths: Vec<_> = builder
.into_iter()
.filter_entry(|p| !is_hidden_entry(p))
.flatten()
.collect();
log_paths_and_assert(&paths, 6 + 1);
Ok(())
}
#[test]
fn match_filter() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "test-files/c-simple/**/*.txt";
let builder = Builder::new(pattern).build(root)?;
let paths: Vec<_> = builder
.into_iter()
.flatten()
.filter(|p| !is_hidden_path(p))
.collect();
log_paths_and_assert(&paths, 6 + 1);
Ok(())
}
#[test]
fn match_with_glob() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "test-files/c-simple/**/*.txt";
let glob = Builder::new("**/test-files/c-simple/a/a[0]/**").build_glob()?;
let paths: Vec<_> = Builder::new(pattern)
.build(root)?
.into_iter()
.flatten()
.filter(|p| !is_hidden_path(p))
.filter(|p| glob.is_match(p))
.collect();
log_paths_and_assert(&paths, 3);
Ok(())
}
#[test]
fn match_with_glob_all() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "test-files/c-simple/**/*.*";
let glob = Builder::new("*.txt").build_glob_set()?;
let paths: Vec<_> = Builder::new(pattern)
.build(root)?
.into_iter()
.filter_entry(|e| !is_hidden_entry(e))
.flatten()
.filter(|p| {
let is_match = glob.is_match(p);
println!("is match: {p:?} - {is_match}");
is_match
})
.collect();
log_paths_and_assert(&paths, 6 + 1);
Ok(())
}
#[test]
fn match_flavours() -> Result<(), String> {
Ok(())
}
#[test]
fn filter_entry_with_glob() -> Result<(), String> {
let root = env!("CARGO_MANIFEST_DIR");
let pattern = "test-files/c-simple/**/*.txt";
let glob = Builder::new(".*").build_glob_set()?;
let paths: Vec<_> = Builder::new(pattern)
.build(root)?
.into_iter()
.filter_entry(|e| !glob.is_match(e))
.flatten()
.collect();
log_paths_and_assert(&paths, 6 + 1);
Ok(())
}
}