1#![doc = include_str!("../README.md")]
22#![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#[derive(Debug)]
105pub struct GlobError(ignore::Error);
106
107pub type WalkError = walkdir::Error;
109pub 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 #[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
152pub 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 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 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 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 pub fn min_depth(mut self, depth: usize) -> Self {
209 self.walker = self.walker.min_depth(depth);
210 self
211 }
212
213 pub fn max_depth(mut self, depth: usize) -> Self {
223 self.walker = self.walker.max_depth(depth);
224 self
225 }
226
227 pub fn follow_links(mut self, yes: bool) -> Self {
239 self.walker = self.walker.follow_links(yes);
240 self
241 }
242
243 pub fn max_open(mut self, n: usize) -> Self {
269 self.walker = self.walker.max_open(n);
270 self
271 }
272
273 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 pub fn contents_first(mut self, yes: bool) -> Self {
297 self.walker = self.walker.contents_first(yes);
298 self
299 }
300
301 pub fn case_insensitive(mut self, yes: bool) -> Self {
305 self.case_insensitive = yes;
306 self
307 }
308
309 pub fn file_type(mut self, file_type: FileType) -> Self {
314 self.file_type = Some(file_type);
315 self
316 }
317
318 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
338pub 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 fn next(&mut self) -> Option<Self::Item> {
356 let mut skip_dir = false;
357
358 'skipper: loop {
361 if skip_dir {
362 self.walker.skip_current_dir();
363 }
364
365 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 let path = e
392 .path()
393 .strip_prefix(self.ignore.path())
394 .unwrap()
395 .to_owned();
396
397 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 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
425pub fn glob_builder<S: AsRef<str>>(pattern: S) -> GlobWalkerBuilder {
430 let path_pattern: PathBuf = pattern.as_ref().into();
432 if path_pattern.is_absolute() {
433 let mut base = PathBuf::new();
435 let mut pattern = PathBuf::new();
436 let mut globbing = false;
437
438 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 GlobWalkerBuilder::new(".", pattern)
464 }
465}
466
467pub 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 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}