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