Skip to main content

ignore/
lib.rs

1/*!
2The ignore crate provides a fast recursive directory iterator that respects
3various filters such as globs, file types and `.gitignore` files. The precise
4matching rules and precedence is explained in the documentation for
5`WalkBuilder`.
6
7Secondarily, this crate exposes gitignore and file type matchers for use cases
8that demand more fine-grained control.
9
10# Example
11
12This example shows the most basic usage of this crate. This code will
13recursively traverse the current directory while automatically filtering out
14files and directories according to ignore globs found in files like
15`.ignore` and `.gitignore`:
16
17
18```rust,no_run
19use ignore::Walk;
20
21for result in Walk::new("./") {
22    // Each item yielded by the iterator is either a directory entry or an
23    // error, so either print the path or the error.
24    match result {
25        Ok(entry) => println!("{}", entry.path().display()),
26        Err(err) => println!("ERROR: {}", err),
27    }
28}
29```
30
31# Example: advanced
32
33By default, the recursive directory iterator will ignore hidden files and
34directories. This can be disabled by building the iterator with `WalkBuilder`:
35
36```rust,no_run
37use ignore::WalkBuilder;
38
39for result in WalkBuilder::new("./").hidden(false).build() {
40    println!("{:?}", result);
41}
42```
43
44See the documentation for `WalkBuilder` for many other options.
45*/
46
47#![deny(missing_docs)]
48
49use std::path::{Path, PathBuf};
50
51pub use crate::walk::{
52    DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
53    WalkParallel, WalkState,
54};
55
56mod default_types;
57mod dir;
58pub mod gitignore;
59pub mod overrides;
60mod pathutil;
61pub mod types;
62mod walk;
63
64/// Represents an error that can occur when parsing a gitignore file.
65#[derive(Debug)]
66pub enum Error {
67    /// A collection of "soft" errors. These occur when adding an ignore
68    /// file partially succeeded.
69    Partial(Vec<Error>),
70    /// An error associated with a specific line number.
71    WithLineNumber {
72        /// The line number.
73        line: u64,
74        /// The underlying error.
75        err: Box<Error>,
76    },
77    /// An error associated with a particular file path.
78    WithPath {
79        /// The file path.
80        path: PathBuf,
81        /// The underlying error.
82        err: Box<Error>,
83    },
84    /// An error associated with a particular directory depth when recursively
85    /// walking a directory.
86    WithDepth {
87        /// The directory depth.
88        depth: usize,
89        /// The underlying error.
90        err: Box<Error>,
91    },
92    /// An error that occurs when a file loop is detected when traversing
93    /// symbolic links.
94    Loop {
95        /// The ancestor file path in the loop.
96        ancestor: PathBuf,
97        /// The child file path in the loop.
98        child: PathBuf,
99    },
100    /// An error that occurs when doing I/O, such as reading an ignore file.
101    Io(std::io::Error),
102    /// An error that occurs when trying to parse a glob.
103    Glob {
104        /// The original glob that caused this error. This glob, when
105        /// available, always corresponds to the glob provided by an end user.
106        /// e.g., It is the glob as written in a `.gitignore` file.
107        ///
108        /// (This glob may be distinct from the glob that is actually
109        /// compiled, after accounting for `gitignore` semantics.)
110        glob: Option<String>,
111        /// The underlying glob error as a string.
112        err: String,
113    },
114    /// A type selection for a file type that is not defined.
115    UnrecognizedFileType(String),
116    /// A user specified file type definition could not be parsed.
117    InvalidDefinition,
118}
119
120impl Clone for Error {
121    fn clone(&self) -> Error {
122        match *self {
123            Error::Partial(ref errs) => Error::Partial(errs.clone()),
124            Error::WithLineNumber { line, ref err } => {
125                Error::WithLineNumber { line, err: err.clone() }
126            }
127            Error::WithPath { ref path, ref err } => {
128                Error::WithPath { path: path.clone(), err: err.clone() }
129            }
130            Error::WithDepth { depth, ref err } => {
131                Error::WithDepth { depth, err: err.clone() }
132            }
133            Error::Loop { ref ancestor, ref child } => Error::Loop {
134                ancestor: ancestor.clone(),
135                child: child.clone(),
136            },
137            Error::Io(ref err) => match err.raw_os_error() {
138                Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
139                None => {
140                    Error::Io(std::io::Error::new(err.kind(), err.to_string()))
141                }
142            },
143            Error::Glob { ref glob, ref err } => {
144                Error::Glob { glob: glob.clone(), err: err.clone() }
145            }
146            Error::UnrecognizedFileType(ref err) => {
147                Error::UnrecognizedFileType(err.clone())
148            }
149            Error::InvalidDefinition => Error::InvalidDefinition,
150        }
151    }
152}
153
154impl Error {
155    /// Returns true if this is a partial error.
156    ///
157    /// A partial error occurs when only some operations failed while others
158    /// may have succeeded. For example, an ignore file may contain an invalid
159    /// glob among otherwise valid globs.
160    pub fn is_partial(&self) -> bool {
161        match *self {
162            Error::Partial(_) => true,
163            Error::WithLineNumber { ref err, .. } => err.is_partial(),
164            Error::WithPath { ref err, .. } => err.is_partial(),
165            Error::WithDepth { ref err, .. } => err.is_partial(),
166            _ => false,
167        }
168    }
169
170    /// Returns true if this error is exclusively an I/O error.
171    pub fn is_io(&self) -> bool {
172        match *self {
173            Error::Partial(ref errs) => errs.len() == 1 && errs[0].is_io(),
174            Error::WithLineNumber { ref err, .. } => err.is_io(),
175            Error::WithPath { ref err, .. } => err.is_io(),
176            Error::WithDepth { ref err, .. } => err.is_io(),
177            Error::Loop { .. } => false,
178            Error::Io(_) => true,
179            Error::Glob { .. } => false,
180            Error::UnrecognizedFileType(_) => false,
181            Error::InvalidDefinition => false,
182        }
183    }
184
185    /// Inspect the original [`std::io::Error`] if there is one.
186    ///
187    /// [`None`] is returned if the [`Error`] doesn't correspond to an
188    /// [`std::io::Error`]. This might happen, for example, when the error was
189    /// produced because a cycle was found in the directory tree while
190    /// following symbolic links.
191    ///
192    /// This method returns a borrowed value that is bound to the lifetime of the [`Error`]. To
193    /// obtain an owned value, the [`into_io_error`] can be used instead.
194    ///
195    /// > This is the original [`std::io::Error`] and is _not_ the same as
196    /// > [`impl From<Error> for std::io::Error`][impl] which contains
197    /// > additional context about the error.
198    ///
199    /// [`None`]: https://doc.rust-lang.org/stable/std/option/enum.Option.html#variant.None
200    /// [`std::io::Error`]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
201    /// [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html
202    /// [`Error`]: struct.Error.html
203    /// [`into_io_error`]: struct.Error.html#method.into_io_error
204    /// [impl]: struct.Error.html#impl-From%3CError%3E
205    pub fn io_error(&self) -> Option<&std::io::Error> {
206        match *self {
207            Error::Partial(ref errs) => {
208                if errs.len() == 1 {
209                    errs[0].io_error()
210                } else {
211                    None
212                }
213            }
214            Error::WithLineNumber { ref err, .. } => err.io_error(),
215            Error::WithPath { ref err, .. } => err.io_error(),
216            Error::WithDepth { ref err, .. } => err.io_error(),
217            Error::Loop { .. } => None,
218            Error::Io(ref err) => Some(err),
219            Error::Glob { .. } => None,
220            Error::UnrecognizedFileType(_) => None,
221            Error::InvalidDefinition => None,
222        }
223    }
224
225    /// Similar to [`io_error`] except consumes self to convert to the original
226    /// [`std::io::Error`] if one exists.
227    ///
228    /// [`io_error`]: struct.Error.html#method.io_error
229    /// [`std::io::Error`]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
230    pub fn into_io_error(self) -> Option<std::io::Error> {
231        match self {
232            Error::Partial(mut errs) => {
233                if errs.len() == 1 {
234                    errs.remove(0).into_io_error()
235                } else {
236                    None
237                }
238            }
239            Error::WithLineNumber { err, .. } => err.into_io_error(),
240            Error::WithPath { err, .. } => err.into_io_error(),
241            Error::WithDepth { err, .. } => err.into_io_error(),
242            Error::Loop { .. } => None,
243            Error::Io(err) => Some(err),
244            Error::Glob { .. } => None,
245            Error::UnrecognizedFileType(_) => None,
246            Error::InvalidDefinition => None,
247        }
248    }
249
250    /// Returns a depth associated with recursively walking a directory (if
251    /// this error was generated from a recursive directory iterator).
252    pub fn depth(&self) -> Option<usize> {
253        match *self {
254            Error::WithPath { ref err, .. } => err.depth(),
255            Error::WithDepth { depth, .. } => Some(depth),
256            _ => None,
257        }
258    }
259
260    /// Turn an error into a tagged error with the given file path.
261    fn with_path<P: AsRef<Path>>(self, path: P) -> Error {
262        Error::WithPath {
263            path: path.as_ref().to_path_buf(),
264            err: Box::new(self),
265        }
266    }
267
268    /// Turn an error into a tagged error with the given depth.
269    fn with_depth(self, depth: usize) -> Error {
270        Error::WithDepth { depth, err: Box::new(self) }
271    }
272
273    /// Turn an error into a tagged error with the given file path and line
274    /// number. If path is empty, then it is omitted from the error.
275    fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
276        let errline =
277            Error::WithLineNumber { line: lineno, err: Box::new(self) };
278        if path.as_ref().as_os_str().is_empty() {
279            return errline;
280        }
281        errline.with_path(path)
282    }
283
284    /// Build an error from a walkdir error.
285    fn from_walkdir(err: walkdir::Error) -> Error {
286        let depth = err.depth();
287        if let (Some(anc), Some(child)) = (err.loop_ancestor(), err.path()) {
288            return Error::WithDepth {
289                depth,
290                err: Box::new(Error::Loop {
291                    ancestor: anc.to_path_buf(),
292                    child: child.to_path_buf(),
293                }),
294            };
295        }
296        let path = err.path().map(|p| p.to_path_buf());
297        let mut ig_err = Error::Io(std::io::Error::from(err));
298        if let Some(path) = path {
299            ig_err = Error::WithPath { path, err: Box::new(ig_err) };
300        }
301        ig_err
302    }
303}
304
305impl std::error::Error for Error {
306    #[allow(deprecated)]
307    fn description(&self) -> &str {
308        match *self {
309            Error::Partial(_) => "partial error",
310            Error::WithLineNumber { ref err, .. } => err.description(),
311            Error::WithPath { ref err, .. } => err.description(),
312            Error::WithDepth { ref err, .. } => err.description(),
313            Error::Loop { .. } => "file system loop found",
314            Error::Io(ref err) => err.description(),
315            Error::Glob { ref err, .. } => err,
316            Error::UnrecognizedFileType(_) => "unrecognized file type",
317            Error::InvalidDefinition => "invalid definition",
318        }
319    }
320}
321
322impl std::fmt::Display for Error {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        match *self {
325            Error::Partial(ref errs) => {
326                let msgs: Vec<String> =
327                    errs.iter().map(|err| err.to_string()).collect();
328                write!(f, "{}", msgs.join("\n"))
329            }
330            Error::WithLineNumber { line, ref err } => {
331                write!(f, "line {}: {}", line, err)
332            }
333            Error::WithPath { ref path, ref err } => {
334                write!(f, "{}: {}", path.display(), err)
335            }
336            Error::WithDepth { ref err, .. } => err.fmt(f),
337            Error::Loop { ref ancestor, ref child } => write!(
338                f,
339                "File system loop found: \
340                           {} points to an ancestor {}",
341                child.display(),
342                ancestor.display()
343            ),
344            Error::Io(ref err) => err.fmt(f),
345            Error::Glob { glob: None, ref err } => write!(f, "{}", err),
346            Error::Glob { glob: Some(ref glob), ref err } => {
347                write!(f, "error parsing glob '{}': {}", glob, err)
348            }
349            Error::UnrecognizedFileType(ref ty) => {
350                write!(f, "unrecognized file type: {}", ty)
351            }
352            Error::InvalidDefinition => write!(
353                f,
354                "invalid definition (format is type:glob, e.g., \
355                           html:*.html)"
356            ),
357        }
358    }
359}
360
361impl From<std::io::Error> for Error {
362    fn from(err: std::io::Error) -> Error {
363        Error::Io(err)
364    }
365}
366
367#[derive(Debug, Default)]
368struct PartialErrorBuilder(Vec<Error>);
369
370impl PartialErrorBuilder {
371    fn push(&mut self, err: Error) {
372        self.0.push(err);
373    }
374
375    fn push_ignore_io(&mut self, err: Error) {
376        if !err.is_io() {
377            self.push(err);
378        }
379    }
380
381    fn maybe_push(&mut self, err: Option<Error>) {
382        if let Some(err) = err {
383            self.push(err);
384        }
385    }
386
387    fn maybe_push_ignore_io(&mut self, err: Option<Error>) {
388        if let Some(err) = err {
389            self.push_ignore_io(err);
390        }
391    }
392
393    fn into_error_option(mut self) -> Option<Error> {
394        if self.0.is_empty() {
395            None
396        } else if self.0.len() == 1 {
397            Some(self.0.pop().unwrap())
398        } else {
399            Some(Error::Partial(self.0))
400        }
401    }
402}
403
404/// The result of a glob match.
405///
406/// The type parameter `T` typically refers to a type that provides more
407/// information about a particular match. For example, it might identify
408/// the specific gitignore file and the specific glob pattern that caused
409/// the match.
410#[derive(Clone, Debug)]
411pub enum Match<T> {
412    /// The path didn't match any glob.
413    None,
414    /// The highest precedent glob matched indicates the path should be
415    /// ignored.
416    Ignore(T),
417    /// The highest precedent glob matched indicates the path should be
418    /// whitelisted.
419    Whitelist(T),
420}
421
422impl<T> Match<T> {
423    /// Returns true if the match result didn't match any globs.
424    pub fn is_none(&self) -> bool {
425        match *self {
426            Match::None => true,
427            Match::Ignore(_) | Match::Whitelist(_) => false,
428        }
429    }
430
431    /// Returns true if the match result implies the path should be ignored.
432    pub fn is_ignore(&self) -> bool {
433        match *self {
434            Match::Ignore(_) => true,
435            Match::None | Match::Whitelist(_) => false,
436        }
437    }
438
439    /// Returns true if the match result implies the path should be
440    /// whitelisted.
441    pub fn is_whitelist(&self) -> bool {
442        match *self {
443            Match::Whitelist(_) => true,
444            Match::None | Match::Ignore(_) => false,
445        }
446    }
447
448    /// Inverts the match so that `Ignore` becomes `Whitelist` and
449    /// `Whitelist` becomes `Ignore`. A non-match remains the same.
450    pub fn invert(self) -> Match<T> {
451        match self {
452            Match::None => Match::None,
453            Match::Ignore(t) => Match::Whitelist(t),
454            Match::Whitelist(t) => Match::Ignore(t),
455        }
456    }
457
458    /// Return the value inside this match if it exists.
459    pub fn inner(&self) -> Option<&T> {
460        match *self {
461            Match::None => None,
462            Match::Ignore(ref t) => Some(t),
463            Match::Whitelist(ref t) => Some(t),
464        }
465    }
466
467    /// Apply the given function to the value inside this match.
468    ///
469    /// If the match has no value, then return the match unchanged.
470    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Match<U> {
471        match self {
472            Match::None => Match::None,
473            Match::Ignore(t) => Match::Ignore(f(t)),
474            Match::Whitelist(t) => Match::Whitelist(f(t)),
475        }
476    }
477
478    /// Return the match if it is not none. Otherwise, return other.
479    pub fn or(self, other: Self) -> Self {
480        if self.is_none() { other } else { self }
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use std::{
487        env, fs,
488        path::{Path, PathBuf},
489    };
490
491    /// A convenient result type alias.
492    pub(crate) type Result<T> =
493        std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
494
495    macro_rules! err {
496        ($($tt:tt)*) => {
497            Box::<dyn std::error::Error + Send + Sync>::from(format!($($tt)*))
498        }
499    }
500
501    /// A simple wrapper for creating a temporary directory that is
502    /// automatically deleted when it's dropped.
503    ///
504    /// We use this in lieu of tempfile because tempfile brings in too many
505    /// dependencies.
506    #[derive(Debug)]
507    pub struct TempDir(PathBuf);
508
509    impl Drop for TempDir {
510        fn drop(&mut self) {
511            fs::remove_dir_all(&self.0).unwrap();
512        }
513    }
514
515    impl TempDir {
516        /// Create a new empty temporary directory under the system's configured
517        /// temporary directory.
518        pub fn new() -> Result<TempDir> {
519            use std::sync::atomic::{AtomicUsize, Ordering};
520
521            static TRIES: usize = 100;
522            static COUNTER: AtomicUsize = AtomicUsize::new(0);
523
524            let tmpdir = env::temp_dir();
525            for _ in 0..TRIES {
526                let count = COUNTER.fetch_add(1, Ordering::Relaxed);
527                let path = tmpdir.join("rust-ignore").join(count.to_string());
528                if path.is_dir() {
529                    continue;
530                }
531                fs::create_dir_all(&path).map_err(|e| {
532                    err!("failed to create {}: {}", path.display(), e)
533                })?;
534                return Ok(TempDir(path));
535            }
536            Err(err!("failed to create temp dir after {} tries", TRIES))
537        }
538
539        /// Return the underlying path to this temporary directory.
540        pub fn path(&self) -> &Path {
541            &self.0
542        }
543    }
544}