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