pistonite_cu/fs/walk2.rs
1use std::ffi::OsStr;
2use std::fs::{FileType, Metadata};
3use std::path::{Component, Path, PathBuf};
4use std::sync::Arc;
5
6use ignore::overrides::OverrideBuilder;
7use ignore::{DirEntry as IgnoreDirEntry, Walk as IgnoreWalk, WalkBuilder as IgnoreWalkBuilder};
8
9use crate::pre::*;
10
11/// Create a walker to walk `root` recursively.
12///
13/// See [`WalkBuilder`] for configurations. [`cu::fs::walk`] can be used directly for the default
14/// configuration.
15///
16/// ```rust,no_run
17/// # use pistonite_cu as cu;
18/// # fn walk_dirs_too() -> cu::Result<()> {
19/// let mut builder = cu::fs::walker(".");
20/// builder.include_dir_entries(true);
21/// for entry in builder.walk()? {
22/// let entry = entry?;
23/// cu::info!("{} (dir: {})", entry.path().display(), entry.is_dir());
24/// }
25/// # Ok(()) }
26/// ```
27#[inline(always)]
28pub fn walker(root: impl AsRef<Path>) -> WalkBuilder {
29 WalkBuilder::new(root.as_ref().to_path_buf())
30}
31
32/// Recursively walk `root` with default settings.
33///
34/// The defaults are:
35/// - includes hidden files,
36/// - does not use `.gitignore` or other ignore files,
37/// - does not follow symbolic links,
38/// - does not apply any glob include/exclude filters,
39/// - yields files only (directory entries are skipped).
40///
41/// To change the configuration, use [`cu::fs::walker`]
42///
43/// ```rust,no_run
44/// # use pistonite_cu as cu;
45/// # fn count_files() -> cu::Result<usize> {
46/// let mut count = 0;
47/// for entry in cu::fs::walk(".")? {
48/// let _entry = entry?;
49/// count += 1;
50/// }
51/// cu::info!("number of files: {count}");
52/// # Ok(count) }
53/// ```
54#[inline(always)]
55pub fn walk(root: impl AsRef<Path>) -> cu::Result<Walk> {
56 walker(root).walk()
57}
58
59/// Builder for a directory [`Walk`], providing a simpler API over the
60/// `ignore` crate for the most common cases.
61///
62/// Created with [`cu::fs::walker`].
63pub struct WalkBuilder {
64 inner: IgnoreWalkBuilder,
65 overrides: OverrideBuilder,
66 has_overrides: bool,
67 include_dir_entries: bool,
68 root: PathBuf,
69}
70
71impl WalkBuilder {
72 fn new(root: PathBuf) -> Self {
73 let mut inner = IgnoreWalkBuilder::new(root.clone());
74 inner.require_git(true);
75 inner.ignore(false);
76 inner.hidden(false);
77 let mut s = Self {
78 inner,
79 overrides: OverrideBuilder::new(root.clone()),
80 has_overrides: false,
81 include_dir_entries: false,
82 root,
83 };
84 s.git(false);
85 s
86 }
87
88 /// Return the inner WalkBuilder from the `ignore` crate for advanced configuration.
89 /// Note that the `overrides` matcher will be replaced when building the walker
90 pub fn as_inner_mut(&mut self) -> &mut IgnoreWalkBuilder {
91 &mut self.inner
92 }
93
94 /// Add glob patterns to include. By default all paths are included.
95 ///
96 /// Patterns use gitignore-style glob syntax (as implemented by the `ignore`
97 /// crate), which supports `*`, `**`, `?`, `[...]` character classes, and
98 /// `{a,b}` brace alternation. Once any include pattern is added, only paths
99 /// matching at least one include (and no exclude) are yielded.
100 ///
101 /// ## Caution
102 /// If the glob pattern starts with `./`, the dot is removed instead of matching
103 /// literal `./`. Patterns starting with `../` still
104 /// matches the literal `../` (meaning the root must start with `../` to produce any match)
105 ///
106 /// ```rust,no_run
107 /// # use pistonite_cu as cu;
108 /// # fn only_sources() -> cu::Result<()> {
109 /// let mut walker = cu::fs::walker(".");
110 /// // include Rust and TOML files anywhere in the tree
111 /// walker.glob_includes(["**/*.{rs,toml}"])?;
112 /// for entry in walker.walk()? {
113 /// cu::info!("{}", entry?.path().display());
114 /// }
115 /// # Ok(()) }
116 /// ```
117 pub fn glob_includes(
118 &mut self,
119 globs: impl IntoIterator<Item = impl AsRef<str>>,
120 ) -> crate::Result<&mut Self> {
121 for g in globs {
122 let g = g.as_ref();
123 if g.starts_with("./") || g.starts_with(".\\") {
124 let g2 = &g[1..];
125 crate::check!(
126 self.overrides.add(g2),
127 "failed to add glob include pattern: '{g2}' (resolved from '{g}')"
128 )?;
129 } else {
130 crate::check!(
131 self.overrides.add(g),
132 "failed to add glob include pattern: '{g}'"
133 )?;
134 }
135 self.has_overrides = true;
136 }
137 Ok(self)
138 }
139
140 /// Add glob patterns to exclude. By default nothing is excluded.
141 ///
142 /// ## Caution
143 /// If the glob pattern starts with `./`, the dot is removed instead of matching
144 /// literal `./`. Patterns starting with `../` still
145 /// matches the literal `../` (meaning the root must start with `../` to produce any match)
146 ///
147 /// ```rust,no_run
148 /// # use pistonite_cu as cu;
149 /// # fn skip_logs_and_tmp() -> cu::Result<()> {
150 /// let mut builder = cu::fs::walker(".");
151 /// builder.glob_excludes(["**/*.{log,tmp}"])?;
152 /// for entry in builder.walk()? {
153 /// cu::info!("{}", entry?.path().display());
154 /// }
155 /// # Ok(()) }
156 /// ```
157 pub fn glob_excludes(
158 &mut self,
159 globs: impl IntoIterator<Item = impl AsRef<str>>,
160 ) -> crate::Result<&mut Self> {
161 let mut s = String::new();
162 s.push('!');
163 for g in globs {
164 let g = g.as_ref();
165 if g.starts_with("./") || g.starts_with(".\\") {
166 let g2 = &g[1..];
167 s.push_str(g2);
168 crate::check!(
169 self.overrides.add(&s),
170 "failed to add glob exclude pattern: '{g2}' (resolved from '{g}')"
171 )?;
172 } else {
173 s.push_str(g);
174 crate::check!(
175 self.overrides.add(&s),
176 "failed to add glob exclude pattern: '{g}'"
177 )?;
178 }
179 s.truncate(1);
180 self.has_overrides = true;
181 }
182 Ok(self)
183 }
184
185 /// Set whether directory entries are returned while iterating. Default is
186 /// `false` (only files are yielded).
187 ///
188 /// When enabled, directory entries and symlinks-to-directories are also
189 /// yielded (as well as the root of the walk, at depth 0). Note that the
190 /// files *inside* a symlinked directory are still not returned unless
191 /// [`follow_links`](Self::follow_links) is also enabled.
192 #[inline(always)]
193 pub fn include_dir_entries(&mut self, include: bool) -> &mut Self {
194 self.include_dir_entries = include;
195 self
196 }
197
198 /// Enable reading `.gitignore` and git exclude configs, (mostly) matching
199 /// git's own behavior. Default is disabled.
200 ///
201 /// Because this relies on git configuration, ignore rules are only applied
202 /// when the walk root is inside a git repository.
203 #[inline(always)]
204 pub fn git(&mut self, yes: bool) -> &mut Self {
205 self.inner.git_global(yes);
206 self.inner.git_ignore(yes);
207 self.inner.git_exclude(yes);
208 self
209 }
210
211 /// Skip hidden files. Default is `false` (i.e. hidden files are
212 /// included).
213 #[inline(always)]
214 pub fn ignore_hidden(&mut self, yes: bool) -> &mut Self {
215 self.inner.hidden(yes);
216 self
217 }
218
219 /// Follow symbolic links. Default is `false`.
220 #[inline(always)]
221 pub fn follow_links(&mut self, yes: bool) -> &mut Self {
222 self.inner.follow_links(yes);
223 self
224 }
225
226 /// Add a custom ignore file name (in addition to any enabled by [`git`](Self::git)).
227 ///
228 /// Files with this name are read as gitignore-style ignore lists. Unlike
229 /// [`git`](Self::git), custom ignore files are honored regardless of whether
230 /// the walk root is inside a git repository.
231 #[inline(always)]
232 pub fn add_ignore_filename(&mut self, ignore_file: &str) -> &mut Self {
233 self.inner.add_custom_ignore_filename(ignore_file);
234 self
235 }
236
237 /// Build the [`Walk`] iterator from this configuration.
238 ///
239 /// Fails if any configured glob patterns cannot be compiled.
240 pub fn walk(mut self) -> cu::Result<Walk> {
241 if self.has_overrides {
242 let overrides = cu::check!(
243 self.overrides.build(),
244 "walk: failed to build glob pattern overrides"
245 )?;
246 self.inner.overrides(overrides);
247 }
248 let walk = self.inner.build();
249 Ok(Walk {
250 inner: walk,
251 include_dir_entries: self.include_dir_entries,
252 root: Arc::new(self.root),
253 })
254 }
255}
256
257/// An iterator over the entries of a directory walk.
258///
259/// Created by [`walk`] or [`WalkBuilder::walk`]. Each item is a
260/// `cu::Result<WalkEntry>`; an `Err` indicates a failure reading a particular
261/// entry (for example, following a broken symlink) and does not necessarily
262/// stop the iteration.
263///
264/// ```rust,no_run
265/// # use pistonite_cu as cu;
266/// # fn list() -> cu::Result<()> {
267/// for entry in cu::fs::walk(".")? {
268/// let entry = entry?;
269/// cu::info!("depth {}: {}", entry.depth(), entry.path().display());
270/// }
271/// # Ok(()) }
272/// ```
273pub struct Walk {
274 inner: IgnoreWalk,
275 include_dir_entries: bool,
276 root: Arc<PathBuf>,
277}
278
279impl Iterator for Walk {
280 type Item = crate::Result<WalkEntry>;
281
282 fn next(&mut self) -> Option<Self::Item> {
283 match self.next_internal() {
284 Err(e) => Some(Err(e)),
285 Ok(None) => None,
286 Ok(Some(e)) => Some(Ok(e)),
287 }
288 }
289}
290
291impl Walk {
292 fn next_internal(&mut self) -> crate::Result<Option<WalkEntry>> {
293 let (entry, file_type) = loop {
294 let entry = crate::some!(self.inner.next());
295 let entry = crate::check!(entry, "walk: failed to read the next entry")?;
296 if entry.depth() == 0 {
297 // skip root node
298 continue;
299 }
300 match entry.file_type() {
301 None => {
302 // stdin
303 continue;
304 }
305 Some(t) => {
306 if self.include_dir_entries {
307 break (entry, t);
308 }
309 // filter out dir entries
310 if t.is_file() {
311 break (entry, t);
312 }
313 if t.is_dir() {
314 crate::trace!(
315 "walk: not emitting entry for directory: '{}'",
316 entry.path().display()
317 );
318 continue;
319 }
320 if t.is_symlink() && entry.path().is_dir() {
321 crate::trace!(
322 "walk: not emitting entry for symlinked directory: '{}'",
323 entry.path().display()
324 );
325 continue;
326 }
327 crate::trace!(
328 "walk: skipping entry with unknown file type: '{}'",
329 entry.path().display()
330 );
331 continue;
332 }
333 }
334 };
335 Ok(Some(WalkEntry {
336 root: Arc::clone(&self.root),
337 inner: entry,
338 file_type,
339 }))
340 }
341}
342
343/// A single entry produced by a [`Walk`].
344///
345/// Provides access to the entry's [`path`](Self::path), its
346/// [`depth`](Self::depth) relative to the walk root, its
347/// [`file_type`](Self::file_type), and lazily-read [`metadata`](Self::metadata).
348#[derive(Debug)]
349pub struct WalkEntry {
350 root: Arc<PathBuf>,
351 inner: IgnoreDirEntry,
352 file_type: FileType,
353}
354
355impl WalkEntry {
356 /// Get the root of the walk
357 #[inline(always)]
358 pub fn root(&self) -> &Path {
359 &self.root
360 }
361
362 /// Get the depth of this entry, `0` is root, `1` is an entry in the root, etc.
363 pub fn depth(&self) -> usize {
364 self.inner.depth()
365 }
366
367 /// Get the path by joining the walk root and the relative
368 /// path of the entry
369 #[inline(always)]
370 pub fn path(&self) -> &Path {
371 self.inner.path()
372 }
373
374 /// Get this entry's path relative to the walk root, without a leading `./`.
375 ///
376 /// Returns `Ok(None)` when the entry *is* the root of the walk (only emitted
377 /// when [`include_dir_entries`](WalkBuilder::include_dir_entries) is
378 /// enabled). Returns an error if the entry path is unexpectedly not
379 /// contained within the walk root.
380 ///
381 /// ```rust,no_run
382 /// # use pistonite_cu as cu;
383 /// # fn print_relative() -> cu::Result<()> {
384 /// for entry in cu::fs::walk("src")? {
385 /// let entry = entry?;
386 /// cu::info!("{}", entry.rel_path()?.display());
387 /// }
388 /// # Ok(()) }
389 /// ```
390 pub fn rel_path(&self) -> cu::Result<PathBuf> {
391 // ensure root is a prefix of inner path
392 let root_norm = self.root.normalize()?;
393 // note we cannot normalize the path after join since it might be a symlink
394 let path_norm = root_norm.join(self.inner.path());
395 let root_iter = root_norm
396 .components()
397 .filter(|x| !matches!(x, Component::CurDir));
398 let mut path_iter = path_norm
399 .components()
400 .filter(|x| !matches!(x, Component::CurDir));
401 for root_comp in root_iter {
402 let path_comp = cu::check!(
403 path_iter.next(),
404 "unexpected: walk entry path is shorter than root"
405 )?;
406 cu::ensure!(
407 root_comp == path_comp,
408 "unexpected: walk entry path is not in root"
409 )?;
410 }
411 let Some(next) = path_iter.next() else {
412 cu::bail!("unexpected: walk entry path is same as root");
413 };
414 let mut p = PathBuf::new();
415 p.push(next);
416 p.extend(path_iter);
417 Ok(p)
418 }
419
420 /// Get the file type
421 #[inline(always)]
422 pub fn file_type(&self) -> FileType {
423 self.file_type
424 }
425
426 /// Check if the entry is a file.
427 #[inline(always)]
428 pub fn is_file(&self) -> bool {
429 self.file_type.is_file()
430 }
431
432 /// Check if the entry is a directory.
433 #[inline(always)]
434 pub fn is_dir(&self) -> bool {
435 self.file_type.is_dir()
436 }
437
438 /// Check if the entry is a symlink.
439 #[inline(always)]
440 pub fn is_symlink(&self) -> bool {
441 self.file_type.is_symlink()
442 }
443
444 /// Get the file name
445 #[inline(always)]
446 pub fn file_name(&self) -> Option<&OsStr> {
447 self.inner.path().file_name()
448 }
449
450 /// Get the entry metadata
451 pub fn metadata(&self) -> crate::Result<Metadata> {
452 crate::check!(
453 self.inner.metadata(),
454 "failed to get metadata for file '{}' while walking directory '{}'",
455 self.inner.path().try_to_rel_from(&*self.root).display(),
456 self.root.display()
457 )
458 }
459}