fs_walk/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! `fs_walk` is a Rust crate for efficiently and flexibly walking the filesystem.
3//! It provides a simple, ergonomic API for recursively traversing directories with fine-grained control over filtering, sorting, and symlink handling.
4//!
5//! ## Why Use `fs_walk`?
6//! - **Flexible filtering**: Filter by file extension, name, regex, or custom predicates.
7//! - **Batch processing**: Process files in chunks for memory efficiency.
8//! - **Symlink handling**: Safely follow symlinks with loop protection.
9//! - **Ergonomic API**: Chainable methods for intuitive configuration.
10//!
11//! ## Features
12//! | Feature | Description |
13//! |-----------------------|-------------------------------------------------------------------------------------------------|
14//! | **Depth control** | Limit traversal depth to avoid unnecessary work. |
15//! | **Result chunking** | Process results in batches for memory efficiency. |
16//! | **Filtering** | Filter by extension, name, regex, or custom predicates. |
17//! | **Symlink support** | Optionally follow symlinks with loop protection. |
18//! | **Sorting** | Sort directory entries for consistent results. |
19//! | **Regex matching** | Enable regex-based filtering (requires the `regex` feature). |
20//!
21//! ## Installation
22//! Add `fs_walk` to your `Cargo.toml`:
23//! ```toml
24//! [dependencies]
25//! fs_walk = "0.2"
26//! ```
27//!
28//! ### Cargo Features
29//! - **`regex`**: Enables regex matching for file and directory names, using the `regex` crate
30//! Enable with:
31//! ```toml
32//! [dependencies]
33//! fs_walk = { version = "0.2", features = ["regex"] }
34//! ```
35//!
36//! ## Usage
37//!
38//! ### Basic Usage
39//! Walk all files and directories in the current directory:
40//! ```rust
41//! use fs_walk::WalkOptions;
42//!
43//! let walker = WalkOptions::new().walk(".");
44//! for path in walker.flatten() {
45//! println!("Found: {:?}", path);
46//! }
47//! ```
48//!
49//! ### Filtering Files
50//! Walk only Rust files (`.rs` extension):
51//! ```rust
52//! use fs_walk::WalkOptions;
53//!
54//! let walker = WalkOptions::new()
55//! .files()
56//! .extension("rs")
57//! .walk(".");
58//! for path in walker.flatten() {
59//! println!("Found Rust file: {:?}", path);
60//! }
61//! ```
62//!
63//! ### Chunking Results
64//! Process files in chunks of 10 for batch operations:
65//! ```rust
66//! use fs_walk::WalkOptions;
67//!
68//! let walker = WalkOptions::new()
69//! .files()
70//! .extension("o")
71//! .walk(".")
72//! .chunks(10);
73//! for chunk in walker {
74//! for path in chunk.iter().flatten() {
75//! println!("Processing: {:?}", path);
76//! }
77//! }
78//! ```
79//!
80//! ### Regex Matching
81//! Walk files matching a regex pattern (requires the `regex` feature):
82//! ```rust
83//! use fs_walk::WalkOptions;
84//!
85//! let walker = WalkOptions::new()
86//! .name_regex(r#"^.*\.rs$"#)
87//! .unwrap()
88//! .walk(".");
89//! for path in walker.flatten() {
90//! println!("Found matching file: {:?}", path);
91//! }
92//! ```
93//!
94//! ### Following Symlinks
95//! Walk directories while following symlinks (with loop protection):
96//! ```rust
97//! use fs_walk::WalkOptions;
98//!
99//! let walker = WalkOptions::new()
100//! .dirs()
101//! .follow_symlink()
102//! .walk(".");
103//! for path in walker.flatten() {
104//! println!("Found directory: {:?}", path);
105//! }
106//! ```
107//!
108//! ## Contributing
109//! Contributions are welcome! If you’d like to report a bug, suggest a feature, or submit a PR, check out the [GitHub repository](https://github.com/yourusername/fs-walk).
110//!
111//! ## License
112//! This project is licensed under the [GPL-3 License](./LICENSE).
113#![deny(unused_imports)]
114
115use std::{
116 collections::{HashSet, VecDeque},
117 fs, io,
118 path::{Path, PathBuf},
119};
120
121#[cfg(feature = "regex")]
122use regex::Regex;
123
124/// Structure encoding the desired walking
125/// options
126///
127/// # Example
128///
129/// ```
130/// use fs_walk;
131///
132/// let w = fs_walk::WalkOptions::new()
133/// // we want to walk only files
134/// .files()
135/// // we want files with .o extension
136/// .extension("o")
137/// .walk("./");
138///
139/// assert!(w.count() > 0);
140/// ```
141#[derive(Debug, Default, Clone)]
142pub struct WalkOptions {
143 sort: bool,
144 dirs_only: bool,
145 files_only: bool,
146 follow_symlink: bool,
147 max_depth: Option<u64>,
148 extensions: HashSet<String>,
149 ends_with: Vec<String>,
150 names: HashSet<String>,
151 #[cfg(feature = "regex")]
152 regex: Vec<Regex>,
153}
154
155impl WalkOptions {
156 /// Create default walking options. The default
157 /// behaviour is to return both files and directories.
158 ///
159 /// # Example
160 ///
161 /// ```
162 /// use fs_walk;
163 /// use std::path::PathBuf;
164 ///
165 /// let o = fs_walk::WalkOptions::new();
166 ///
167 /// let paths: Vec<PathBuf> = o.walk("./").flatten().collect();
168 ///
169 /// assert!(paths.iter().any(|p| p.is_dir()));
170 /// assert!(paths.iter().any(|p| p.is_file()));
171 /// ```
172 #[inline(always)]
173 pub fn new() -> Self {
174 Self::default()
175 }
176
177 /// Configure walking option to return only
178 /// directories
179 ///
180 /// # Example
181 ///
182 /// ```
183 /// use fs_walk::WalkOptions;
184 ///
185 /// for p in WalkOptions::new().dirs().walk("./").flatten() {
186 /// assert!(p.is_dir());
187 /// }
188 /// ```
189 #[inline(always)]
190 pub fn dirs(&mut self) -> &mut Self {
191 self.dirs_only = true;
192 self
193 }
194
195 /// Configure walking option to return only
196 /// files
197 ///
198 /// # Example
199 ///
200 /// ```
201 /// use fs_walk::WalkOptions;
202 ///
203 /// for p in WalkOptions::new().files().walk("./").flatten() {
204 /// assert!(p.is_file());
205 /// }
206 /// ```
207 #[inline(always)]
208 pub fn files(&mut self) -> &mut Self {
209 self.files_only = true;
210 self
211 }
212
213 /// Configures the walker to follow symbolic links during traversal.
214 ///
215 /// By default, the walker does **not** follow symbolic links.
216 /// When this option is enabled, the walker will recursively traverse
217 /// into directories pointed to by symbolic links, as if they were real directories.
218 ///
219 /// # Symlink Loop Protection
220 /// The walker is protected against infinite loops caused by cyclic symlinks.
221 /// It uses the canonical path and a hash set of visited directories (via BLAKE3 hashing)
222 /// to ensure each directory is only visited once, even if it is linked multiple times.
223 ///
224 /// # Example
225 ///
226 /// ```rust
227 /// use fs_walk::WalkOptions;
228 ///
229 /// // Create a walker that follows symlinks
230 /// let mut options = WalkOptions::new();
231 /// options.follow_symlink();
232 ///
233 /// // Now symlinks to directories will be traversed
234 /// for entry in options.walk("./").flatten() {
235 /// println!("{:?}", entry);
236 /// }
237 /// ```
238 ///
239 /// # Safety
240 /// While the walker is protected against symlink loops, be cautious when enabling this option
241 /// in untrusted directories, as it may still expose your program to other symlink-based attacks.
242 pub fn follow_symlink(&mut self) -> &mut Self {
243 self.follow_symlink = true;
244 self
245 }
246
247 /// Configure a maximum depth for the walker. If no depth
248 /// is specified the walker will walk through all directories
249 /// in a BFS way.
250 ///
251 /// # Example
252 ///
253 /// ```
254 /// use fs_walk::WalkOptions;
255 /// use std::path::Path;
256 ///
257 /// for p in WalkOptions::new().max_depth(0).walk("./").flatten() {
258 /// assert_eq!(p.parent(), Some(Path::new(".")));
259 /// }
260 ///
261 /// ```
262 #[inline(always)]
263 pub fn max_depth(&mut self, depth: u64) -> &mut Self {
264 self.max_depth = Some(depth);
265 self
266 }
267
268 /// Configure walker to return only files matching file extension.
269 /// For any file, if [Path::extension] is not [None] it will be
270 /// checked against `ext`. This function can be called several
271 /// times to return files matching one of the desired extension.
272 ///
273 /// See [Path::extension] for the correct way to specify `ext`.
274 ///
275 /// # Example
276 /// ```
277 /// use fs_walk;
278 /// use std::path::PathBuf;
279 /// use std::ffi::OsStr;
280 ///
281 /// let wk = fs_walk::WalkOptions::new()
282 /// .files()
283 /// .extension("o")
284 /// .extension("rs")
285 /// .walk("./");
286 ///
287 /// let paths: Vec<PathBuf> = wk.flatten().collect();
288 ///
289 /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("o"))));
290 /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("rs"))));
291 /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("toml"))));
292 /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("lock"))));
293 /// ```
294 #[inline(always)]
295 pub fn extension<S: AsRef<str>>(&mut self, ext: S) -> &mut Self {
296 self.extensions.insert(ext.as_ref().to_string());
297 self
298 }
299
300 /// Configure walker to return files ending with pattern `pat`
301 /// For any file, if [Path::to_string_lossy] **ends with** pattern
302 /// `pat` it is going to be returned.
303 ///
304 /// This method can be used to match path with double extensions (i.e. `.txt.gz`)
305 /// without having to do manual pattern matching on walker's output.
306 ///
307 /// See [str::ends_with] for more detail about matching
308 ///
309 /// # Example
310 /// ```
311 /// use fs_walk;
312 /// use std::path::PathBuf;
313 /// use std::ffi::OsStr;
314 ///
315 /// let wk = fs_walk::WalkOptions::new()
316 /// .files()
317 /// .extension("o")
318 /// // we can put . here not in extension
319 /// // can be used to match path with double extensions
320 /// .ends_with(".rs")
321 /// .walk("./");
322 ///
323 /// let paths: Vec<PathBuf> = wk.flatten().collect();
324 ///
325 /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("o"))));
326 /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("rs"))));
327 /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("toml"))));
328 /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("lock"))));
329 /// ```
330 #[inline(always)]
331 pub fn ends_with<S: AsRef<str>>(&mut self, pat: S) -> &mut Self {
332 self.ends_with.push(pat.as_ref().to_string());
333 self
334 }
335
336 /// Configure walker to return only paths with [Path::file_name] matching `name`
337 ///
338 /// # Example
339 ///
340 /// ```
341 /// use fs_walk;
342 /// use std::path::PathBuf;
343 /// use std::ffi::OsStr;
344 ///
345 /// let wk = fs_walk::WalkOptions::new()
346 /// .name("lib.rs")
347 /// .walk("./");
348 ///
349 /// let paths: Vec<PathBuf> = wk.flatten().collect();
350 ///
351 /// assert!(paths.iter().all(|p| p.file_name() == Some(OsStr::new("lib.rs"))));
352 /// assert!(paths.iter().all(|p| p.is_file()));
353 /// ```
354 #[inline(always)]
355 pub fn name<S: AsRef<str>>(&mut self, name: S) -> &mut Self {
356 self.names.insert(name.as_ref().to_string());
357 self
358 }
359
360 /// Configure walker to return only paths with [Path::file_name] matching `regex`
361 ///
362 /// # Example
363 ///
364 /// ```
365 /// use fs_walk;
366 /// use std::path::PathBuf;
367 /// use std::ffi::OsStr;
368 ///
369 /// let w = fs_walk::WalkOptions::new()
370 /// .name_regex(r#"^(.*\.rs|src|target)$"#)
371 /// .unwrap()
372 /// .walk("./");
373 /// let paths: Vec<PathBuf> = w.flatten().collect();
374 /// assert!(paths.iter().any(|p| p.is_dir()));
375 /// assert!(paths.iter().any(|p| p.is_file()));
376 /// ```
377 #[inline(always)]
378 #[cfg(feature = "regex")]
379 #[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
380 pub fn name_regex<S: AsRef<str>>(&mut self, regex: S) -> Result<&mut Self, regex::Error> {
381 self.regex.push(Regex::new(regex.as_ref())?);
382 Ok(self)
383 }
384
385 #[inline(always)]
386 fn regex_is_empty(&self) -> bool {
387 #[cfg(feature = "regex")]
388 return self.regex.is_empty();
389 #[cfg(not(feature = "regex"))]
390 true
391 }
392
393 #[inline(always)]
394 fn path_match<P: AsRef<Path>>(&self, p: P) -> bool {
395 let p = p.as_ref();
396
397 if self.extensions.is_empty()
398 && self.ends_with.is_empty()
399 && self.names.is_empty()
400 && self.regex_is_empty()
401 {
402 return true;
403 }
404
405 // test filename
406 if !self.names.is_empty()
407 && p.file_name()
408 .and_then(|file_name| file_name.to_str())
409 .map(|file_name| self.names.contains(file_name))
410 .unwrap_or_default()
411 {
412 return true;
413 }
414
415 // we check for extension
416 if !self.extensions.is_empty()
417 && p.extension()
418 .and_then(|ext| ext.to_str())
419 .map(|ext| self.extensions.contains(ext))
420 .unwrap_or_default()
421 {
422 return true;
423 }
424
425 // we check for paths ending with pattern
426 for trail in self.ends_with.iter() {
427 if p.to_string_lossy().ends_with(trail) {
428 return true;
429 }
430 }
431
432 // we check regex
433 #[cfg(feature = "regex")]
434 if let Some(file_name) = p.file_name().and_then(|n| n.to_str()) {
435 for re in self.regex.iter() {
436 if re.is_match(file_name) {
437 return true;
438 }
439 }
440 }
441
442 false
443 }
444
445 /// Sorts entries at every directory listing
446 #[inline(always)]
447 pub fn sort(&mut self, value: bool) -> &mut Self {
448 self.sort = value;
449 self
450 }
451
452 /// Turns [self] into a [Walker]
453 #[inline(always)]
454 pub fn walk<P: AsRef<Path>>(&self, p: P) -> Walker {
455 Walker::from_path(p).with_options(self.clone())
456 }
457}
458
459pub struct Chunks {
460 it: Walker,
461 capacity: usize,
462}
463
464impl Iterator for Chunks {
465 type Item = Vec<Result<PathBuf, io::Error>>;
466
467 #[inline]
468 fn next(&mut self) -> Option<Self::Item> {
469 let out: Self::Item = self.it.by_ref().take(self.capacity).collect();
470 if out.is_empty() {
471 return None;
472 }
473 Some(out)
474 }
475}
476
477#[derive(Debug)]
478struct PathIterator {
479 depth: u64,
480 path: Option<PathBuf>,
481 items: VecDeque<Result<PathBuf, io::Error>>,
482 init: bool,
483 sort: bool,
484}
485
486impl PathIterator {
487 fn new<P: AsRef<Path>>(depth: u64, path: P, sort: bool) -> Self {
488 Self {
489 depth,
490 path: Some(path.as_ref().to_path_buf()),
491 items: VecDeque::new(),
492 init: false,
493 sort,
494 }
495 }
496}
497
498impl Iterator for PathIterator {
499 type Item = Result<PathBuf, io::Error>;
500 fn next(&mut self) -> Option<Self::Item> {
501 if !self.init {
502 self.init = true;
503 // guarantee to exist at init
504 let path = self.path.as_ref().unwrap();
505
506 if path.is_file() {
507 match self.path.take() {
508 Some(p) => return Some(Ok(p)),
509 None => return None,
510 }
511 } else {
512 match fs::read_dir(path) {
513 Ok(rd) => {
514 let mut tmp: Vec<Result<PathBuf, io::Error>> =
515 rd.map(|r| r.map(|de| de.path())).collect();
516
517 if self.sort {
518 tmp.sort_by(|res1, res2| {
519 match (res1, res2) {
520 (Ok(path1), Ok(path2)) => path1.cmp(path2), // Compare paths if both are Ok
521 (Err(_), Ok(_)) => std::cmp::Ordering::Greater, // Err comes after Ok
522 (Ok(_), Err(_)) => std::cmp::Ordering::Less, // Ok comes before Err
523 (Err(e1), Err(e2)) => e1.to_string().cmp(&e2.to_string()), // Compare errors by message
524 }
525 });
526 }
527
528 self.items.extend(tmp);
529 }
530 Err(e) => self.items.push_back(Err(e)),
531 };
532 }
533 }
534
535 self.items.pop_front()
536 }
537}
538
539#[derive(Debug, Default)]
540pub struct Walker {
541 init: bool,
542 root: PathBuf,
543 options: WalkOptions,
544 queue: VecDeque<PathIterator>,
545 current: Option<PathIterator>,
546 marked: HashSet<[u8; 32]>,
547}
548
549impl Walker {
550 /// Creates a [Walker] with default [WalkOptions] configured
551 /// to walk path `p`.
552 ///
553 /// If `p` is a file, only that file will be returned when
554 /// iterating over the [Walker]
555 ///
556 /// # Example
557 ///
558 /// ```
559 /// use fs_walk::Walker;
560 ///
561 /// assert!(Walker::from_path("./").count() > 0)
562 /// ```
563 #[inline(always)]
564 pub fn from_path<P: AsRef<Path>>(p: P) -> Self {
565 Self {
566 root: p.as_ref().to_path_buf(),
567 ..Default::default()
568 }
569 }
570
571 /// Creates a [Walker] with a [WalkOptions] configured
572 /// to walk path `p`.
573 ///
574 /// If `p` is a file, only that file will be returned when
575 /// iterating over the [Walker]
576 ///
577 /// # Example
578 ///
579 /// ```
580 /// use fs_walk::{Walker, WalkOptions};
581 ///
582 /// let mut o = WalkOptions::new();
583 /// o.files();
584 ///
585 /// assert!(Walker::from_path("./").with_options(o.clone()).flatten().any(|p| p.is_file()));
586 /// assert!(!Walker::from_path("./").with_options(o).flatten().any(|p| p.is_dir()));
587 /// ```
588 #[inline(always)]
589 pub fn with_options(mut self, o: WalkOptions) -> Self {
590 self.options = o;
591 self
592 }
593
594 /// Returns a [Chunks] iterator allowing to process [Walker] data
595 /// in chunks of desired `size`.
596 ///
597 /// # Example
598 ///
599 /// ```
600 /// use fs_walk::{Walker, WalkOptions};
601 ///
602 ///
603 /// for chunk in Walker::from_path("./").chunks(1) {
604 /// assert_eq!(chunk.len(), 1);
605 /// for p in chunk.iter().flatten() {
606 /// assert!(p.is_dir() || p.is_file());
607 /// }
608 /// }
609 /// ```
610 #[inline(always)]
611 pub fn chunks(self, size: usize) -> Chunks {
612 Chunks {
613 it: self,
614 capacity: size,
615 }
616 }
617
618 #[inline(always)]
619 fn initialize(&mut self) {
620 if let Ok(can) = self.root.canonicalize() {
621 let h = blake3::hash(can.to_string_lossy().as_bytes());
622 self.current = Some(PathIterator::new(0, &self.root, self.options.sort));
623 self.marked.insert(h.into());
624 }
625 self.init = true
626 }
627
628 #[inline(always)]
629 fn _next(&mut self) -> Option<Result<PathBuf, io::Error>> {
630 if !self.init {
631 self.initialize();
632 }
633
634 let Some(pi) = self.current.as_mut() else {
635 if self.queue.is_empty() {
636 return None;
637 } else {
638 self.current = self.queue.pop_back();
639 return self._next();
640 }
641 };
642
643 let depth = pi.depth;
644 let ni = pi.next();
645
646 match ni {
647 Some(Ok(p)) => {
648 if p.is_file() {
649 Some(Ok(p))
650 } else {
651 let next_depth = pi.depth + 1;
652 if let Some(max_depth) = self.options.max_depth {
653 if next_depth > max_depth {
654 return Some(Ok(p));
655 }
656 }
657
658 // we use canonical path for marking directories
659 if let Ok(can) = p.canonicalize() {
660 let mut must_walk = false;
661
662 if p.is_symlink() && self.options.follow_symlink {
663 let h = blake3::hash(can.to_string_lossy().as_bytes());
664
665 if !self.marked.contains(h.as_bytes()) {
666 must_walk |= true;
667 self.marked.insert(h.into());
668 }
669 }
670
671 if must_walk || !p.is_symlink() {
672 // current cannot be null here
673 let pi = self.current.take().unwrap();
674 // we push our ongoing iterator to the queue
675 // to process dfs
676 self.queue.push_back(pi);
677 self.current = Some(PathIterator::new(depth + 1, &p, self.options.sort))
678 }
679 }
680
681 Some(Ok(p))
682 }
683 }
684 Some(Err(e)) => Some(Err(e)),
685 None => {
686 self.current = self.queue.pop_back();
687 self._next()
688 }
689 }
690 }
691}
692
693impl Iterator for Walker {
694 type Item = Result<PathBuf, io::Error>;
695
696 #[inline]
697 fn next(&mut self) -> Option<Self::Item> {
698 while let Some(item) = self._next() {
699 if item.is_err() {
700 return Some(item);
701 }
702 match item {
703 Ok(p) => {
704 if p.is_dir()
705 && (!self.options.files_only || self.options.dirs_only)
706 && self.options.path_match(&p)
707 {
708 return Some(Ok(p));
709 }
710
711 if p.is_file()
712 && (!self.options.dirs_only || self.options.files_only)
713 && self.options.path_match(&p)
714 {
715 return Some(Ok(p));
716 }
717 }
718 Err(e) => return Some(Err(e)),
719 }
720 }
721 None
722 }
723}
724
725#[cfg(test)]
726mod tests {
727
728 use std::ffi::OsStr;
729
730 use super::*;
731
732 #[test]
733 fn test_walker() {
734 let w = Walker::from_path("./");
735 for e in w.flatten() {
736 println!("{e:?}")
737 }
738 }
739
740 #[test]
741 fn test_walker_on_file() {
742 // walking on a file doesn't return any error instead
743 // it returns only the file
744 let w = WalkOptions::new().walk("./src/lib.rs");
745 let v = w.flatten().collect::<Vec<PathBuf>>();
746
747 assert_eq!(v.len(), 1)
748 }
749
750 #[test]
751 fn test_walker_only_files() {
752 let mut o = WalkOptions::new();
753 o.files();
754 let w = o.walk("./");
755
756 for p in w.flatten() {
757 assert!(p.is_file())
758 }
759 }
760
761 #[test]
762 fn test_files_by_extension() {
763 let mut o = WalkOptions::new();
764 o.files().extension("o");
765
766 let w = o.walk("./");
767
768 let mut c = 0;
769 for p in w.flatten() {
770 assert_eq!(p.extension(), Some(OsStr::new("o")));
771 c += 1;
772 }
773 assert!(c > 0);
774 }
775
776 #[test]
777 fn test_files_ends_with() {
778 let mut o = WalkOptions::new();
779 o.ends_with(".o");
780 let w = o.walk("./");
781
782 let mut c = 0;
783 for p in w.flatten() {
784 assert_eq!(p.extension(), Some(OsStr::new("o")));
785 c += 1;
786 }
787 assert!(c > 0);
788 }
789
790 #[test]
791 fn test_dirs_ends_with() {
792 let mut o = WalkOptions::new();
793 o.ends_with("src").ends_with(".git");
794 let v = o.walk("./").flatten().collect::<Vec<PathBuf>>();
795
796 assert!(v.len() >= 2);
797 for p in v.iter() {
798 assert!(p.is_dir());
799 }
800 }
801
802 #[test]
803 fn test_files_by_chunks_and_extension() {
804 let mut o = WalkOptions::new();
805 o.files().extension("o");
806 let w = o.walk("./");
807
808 let mut c = 0;
809 for chunk in w.chunks(1) {
810 assert_eq!(chunk.len(), 1);
811 for p in chunk.iter().flatten() {
812 assert_eq!(p.extension(), Some(OsStr::new("o")));
813 c += 1;
814 }
815 }
816 assert!(c > 0);
817 }
818
819 #[test]
820 fn test_walker_only_dirs() {
821 let mut o = WalkOptions::new();
822 o.dirs();
823
824 let w = o.walk("./");
825
826 for p in w.flatten() {
827 assert!(p.is_dir());
828 }
829 }
830
831 #[test]
832 fn test_walker_dirs_and_files() {
833 let mut o = WalkOptions::new();
834 o.dirs().files();
835 let w = o.walk("./");
836
837 for p in w.flatten() {
838 assert!(p.is_dir() || p.is_file());
839 }
840 }
841
842 #[test]
843 fn test_max_depth() {
844 let d0 = WalkOptions::new().max_depth(0).walk("./").count();
845 let d1 = WalkOptions::new().max_depth(1).walk("./").count();
846
847 println!("d0={d0} d1={d1}");
848 // we verify that at depth 0 we have less items than at depth 1
849 assert!(d1 > d0);
850 }
851
852 #[test]
853 fn test_sort() {
854 let w = WalkOptions::new().max_depth(0).sort(true).walk("./");
855 let ns = WalkOptions::new().max_depth(0).sort(false).walk("./");
856
857 let sorted = w.flatten().collect::<Vec<PathBuf>>();
858 let mut unsorted = ns.flatten().collect::<Vec<PathBuf>>();
859
860 assert!(sorted.len() > 1);
861 assert_ne!(sorted, unsorted);
862 unsorted.sort();
863 assert_eq!(sorted, unsorted);
864 }
865
866 #[test]
867 fn test_name() {
868 let w = WalkOptions::new().name("lib.rs").name("src").walk("./");
869
870 let v = w.flatten().collect::<Vec<PathBuf>>();
871 assert!(v.len() > 1);
872 for p in v.iter() {
873 if p.file_name().unwrap() == "lib.rs" {
874 assert!(p.is_file())
875 }
876 if p.file_name().unwrap() == "src" {
877 assert!(p.is_dir())
878 }
879 }
880 }
881
882 #[test]
883 #[cfg(feature = "regex")]
884 fn test_name_regex() {
885 let mut w = WalkOptions::new();
886
887 w.name_regex(r#"^(.*\.rs|src|target)$"#)
888 .unwrap()
889 .name_regex(r#".*\.md"#)
890 .unwrap();
891
892 assert!(w.clone().dirs().walk("./").count() > 0);
893 assert!(w.clone().files().walk("./").count() > 0);
894 }
895
896 #[test]
897 fn test_walker_follow_symlink() {
898 use std::os::unix::fs::symlink;
899 use tempfile::{tempdir, Builder};
900
901 // Create a temporary directory and a file inside it
902 let dir = tempdir().unwrap();
903 let test_dir_path = dir.path().join("test_dir");
904 fs::create_dir(&test_dir_path).unwrap();
905 let file_path = test_dir_path.join("test_file.txt");
906 fs::File::create(&file_path).unwrap();
907
908 // Create a symlink to the temp directory
909 let symlink_path = Builder::new().prefix("symlink_test").tempdir().unwrap();
910 symlink(&dir, symlink_path.path().join("symlink")).unwrap();
911 symlink(&symlink_path, symlink_path.path().join("loop")).unwrap();
912
913 // Test with follow_symlink=true
914 let paths = WalkOptions::new()
915 .follow_symlink()
916 .files()
917 .walk(&symlink_path)
918 .flatten()
919 .collect::<Vec<PathBuf>>();
920 // Should find the file inside the symlink's target
921 assert_eq!(paths.len(), 1);
922 assert!(paths[0].ends_with("test_file.txt"));
923
924 // Test with follow_symlink=false (default)
925 let paths = WalkOptions::new()
926 .files()
927 .walk(&symlink_path)
928 .flatten()
929 .collect::<Vec<PathBuf>>();
930 // Should NOT find the file inside the symlink's target
931 assert!(paths.is_empty());
932
933 // Test dir with follow_symlink=false (default)
934 let paths = WalkOptions::new()
935 .dirs()
936 .walk(&symlink_path)
937 .flatten()
938 .collect::<Vec<PathBuf>>();
939 assert!(paths.iter().any(|p| p.ends_with("loop")));
940 assert!(paths.iter().any(|p| p.ends_with("symlink")));
941 assert!(!paths.iter().any(|p| p == &test_dir_path));
942
943 // Test dirs with follow_symlink=true
944 let paths = WalkOptions::new()
945 .dirs()
946 .follow_symlink()
947 .walk(&symlink_path)
948 .flatten()
949 .collect::<Vec<PathBuf>>();
950 println!("{paths:#?}");
951 println!("{test_dir_path:?}");
952 assert!(paths.iter().any(|p| p.ends_with("loop")));
953 assert!(paths.iter().any(|p| p.ends_with("symlink")));
954 assert!(paths
955 .iter()
956 .any(|p| p.canonicalize().unwrap() == test_dir_path));
957 }
958}