mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-08-31 20:33:55 -07:00
This adds a new walk type in the `ignore` crate, `WalkParallel`, which provides a way for recursively iterating over a set of paths in parallel while respecting various ignore rules. The API is a bit strange, as a closure producing a closure isn't something one often sees, but it does seem to work well. This also allowed us to simplify much of the worker logic in ripgrep proper, where MultiWorker is now gone.
361 lines
11 KiB
Rust
361 lines
11 KiB
Rust
/*!
|
|
The ignore crate provides a fast recursive directory iterator that respects
|
|
various filters such as globs, file types and `.gitignore` files. The precise
|
|
matching rules and precedence is explained in the documentation for
|
|
`WalkBuilder`.
|
|
|
|
Secondarily, this crate exposes gitignore and file type matchers for use cases
|
|
that demand more fine-grained control.
|
|
|
|
# Example
|
|
|
|
This example shows the most basic usage of this crate. This code will
|
|
recursively traverse the current directory while automatically filtering out
|
|
files and directories according to ignore globs found in files like
|
|
`.ignore` and `.gitignore`:
|
|
|
|
|
|
```rust,no_run
|
|
use ignore::Walk;
|
|
|
|
for result in Walk::new("./") {
|
|
// Each item yielded by the iterator is either a directory entry or an
|
|
// error, so either print the path or the error.
|
|
match result {
|
|
Ok(entry) => println!("{}", entry.path().display()),
|
|
Err(err) => println!("ERROR: {}", err),
|
|
}
|
|
}
|
|
```
|
|
|
|
# Example: advanced
|
|
|
|
By default, the recursive directory iterator will ignore hidden files and
|
|
directories. This can be disabled by building the iterator with `WalkBuilder`:
|
|
|
|
```rust,no_run
|
|
use ignore::WalkBuilder;
|
|
|
|
for result in WalkBuilder::new("./").hidden(false).build() {
|
|
println!("{:?}", result);
|
|
}
|
|
```
|
|
|
|
See the documentation for `WalkBuilder` for many other options.
|
|
*/
|
|
|
|
extern crate crossbeam;
|
|
extern crate globset;
|
|
#[macro_use]
|
|
extern crate lazy_static;
|
|
#[macro_use]
|
|
extern crate log;
|
|
extern crate memchr;
|
|
extern crate regex;
|
|
#[cfg(test)]
|
|
extern crate tempdir;
|
|
extern crate thread_local;
|
|
extern crate walkdir;
|
|
|
|
use std::error;
|
|
use std::fmt;
|
|
use std::io;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
pub use walk::{DirEntry, Walk, WalkBuilder, WalkParallel, WalkState};
|
|
|
|
mod dir;
|
|
pub mod gitignore;
|
|
mod pathutil;
|
|
pub mod overrides;
|
|
pub mod types;
|
|
mod walk;
|
|
|
|
/// Represents an error that can occur when parsing a gitignore file.
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
/// A collection of "soft" errors. These occur when adding an ignore
|
|
/// file partially succeeded.
|
|
Partial(Vec<Error>),
|
|
/// An error associated with a specific line number.
|
|
WithLineNumber { line: u64, err: Box<Error> },
|
|
/// An error associated with a particular file path.
|
|
WithPath { path: PathBuf, err: Box<Error> },
|
|
/// An error associated with a particular directory depth when recursively
|
|
/// walking a directory.
|
|
WithDepth { depth: usize, err: Box<Error> },
|
|
/// An error that occurs when a file loop is detected when traversing
|
|
/// symbolic links.
|
|
Loop { ancestor: PathBuf, child: PathBuf },
|
|
/// An error that occurs when doing I/O, such as reading an ignore file.
|
|
Io(io::Error),
|
|
/// An error that occurs when trying to parse a glob.
|
|
Glob(String),
|
|
/// A type selection for a file type that is not defined.
|
|
UnrecognizedFileType(String),
|
|
/// A user specified file type definition could not be parsed.
|
|
InvalidDefinition,
|
|
}
|
|
|
|
impl Error {
|
|
/// Returns true if this is a partial error.
|
|
///
|
|
/// A partial error occurs when only some operations failed while others
|
|
/// may have succeeded. For example, an ignore file may contain an invalid
|
|
/// glob among otherwise valid globs.
|
|
pub fn is_partial(&self) -> bool {
|
|
match *self {
|
|
Error::Partial(_) => true,
|
|
Error::WithLineNumber { ref err, .. } => err.is_partial(),
|
|
Error::WithPath { ref err, .. } => err.is_partial(),
|
|
Error::WithDepth { ref err, .. } => err.is_partial(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns true if this error is exclusively an I/O error.
|
|
pub fn is_io(&self) -> bool {
|
|
match *self {
|
|
Error::Partial(ref errs) => errs.len() == 1 && errs[0].is_io(),
|
|
Error::WithLineNumber { ref err, .. } => err.is_io(),
|
|
Error::WithPath { ref err, .. } => err.is_io(),
|
|
Error::WithDepth { ref err, .. } => err.is_io(),
|
|
Error::Loop { .. } => false,
|
|
Error::Io(_) => true,
|
|
Error::Glob(_) => false,
|
|
Error::UnrecognizedFileType(_) => false,
|
|
Error::InvalidDefinition => false,
|
|
}
|
|
}
|
|
|
|
/// Returns a depth associated with recursively walking a directory (if
|
|
/// this error was generated from a recursive directory iterator).
|
|
pub fn depth(&self) -> Option<usize> {
|
|
match *self {
|
|
Error::WithPath { ref err, .. } => err.depth(),
|
|
Error::WithDepth { depth, .. } => Some(depth),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Turn an error into a tagged error with the given file path.
|
|
fn with_path<P: AsRef<Path>>(self, path: P) -> Error {
|
|
Error::WithPath {
|
|
path: path.as_ref().to_path_buf(),
|
|
err: Box::new(self),
|
|
}
|
|
}
|
|
|
|
/// Turn an error into a tagged error with the given depth.
|
|
fn with_depth(self, depth: usize) -> Error {
|
|
Error::WithDepth {
|
|
depth: depth,
|
|
err: Box::new(self),
|
|
}
|
|
}
|
|
|
|
/// Turn an error into a tagged error with the given file path and line
|
|
/// number. If path is empty, then it is omitted from the error.
|
|
fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
|
|
let errline = Error::WithLineNumber {
|
|
line: lineno,
|
|
err: Box::new(self),
|
|
};
|
|
if path.as_ref().as_os_str().is_empty() {
|
|
return errline;
|
|
}
|
|
errline.with_path(path)
|
|
}
|
|
}
|
|
|
|
impl error::Error for Error {
|
|
fn description(&self) -> &str {
|
|
match *self {
|
|
Error::Partial(_) => "partial error",
|
|
Error::WithLineNumber { ref err, .. } => err.description(),
|
|
Error::WithPath { ref err, .. } => err.description(),
|
|
Error::WithDepth { ref err, .. } => err.description(),
|
|
Error::Loop { .. } => "file system loop found",
|
|
Error::Io(ref err) => err.description(),
|
|
Error::Glob(ref msg) => msg,
|
|
Error::UnrecognizedFileType(_) => "unrecognized file type",
|
|
Error::InvalidDefinition => "invalid definition",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Error {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match *self {
|
|
Error::Partial(ref errs) => {
|
|
let msgs: Vec<String> =
|
|
errs.iter().map(|err| err.to_string()).collect();
|
|
write!(f, "{}", msgs.join("\n"))
|
|
}
|
|
Error::WithLineNumber { line, ref err } => {
|
|
write!(f, "line {}: {}", line, err)
|
|
}
|
|
Error::WithPath { ref path, ref err } => {
|
|
write!(f, "{}: {}", path.display(), err)
|
|
}
|
|
Error::WithDepth { ref err, .. } => err.fmt(f),
|
|
Error::Loop { ref ancestor, ref child } => {
|
|
write!(f, "File system loop found: \
|
|
{} points to an ancestor {}",
|
|
child.display(), ancestor.display())
|
|
}
|
|
Error::Io(ref err) => err.fmt(f),
|
|
Error::Glob(ref msg) => write!(f, "{}", msg),
|
|
Error::UnrecognizedFileType(ref ty) => {
|
|
write!(f, "unrecognized file type: {}", ty)
|
|
}
|
|
Error::InvalidDefinition => {
|
|
write!(f, "invalid definition (format is type:glob, e.g., \
|
|
html:*.html)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<io::Error> for Error {
|
|
fn from(err: io::Error) -> Error {
|
|
Error::Io(err)
|
|
}
|
|
}
|
|
|
|
impl From<walkdir::Error> for Error {
|
|
fn from(err: walkdir::Error) -> Error {
|
|
let depth = err.depth();
|
|
if let (Some(anc), Some(child)) = (err.loop_ancestor(), err.path()) {
|
|
return Error::WithDepth {
|
|
depth: depth,
|
|
err: Box::new(Error::Loop {
|
|
ancestor: anc.to_path_buf(),
|
|
child: child.to_path_buf(),
|
|
}),
|
|
};
|
|
}
|
|
let path = err.path().map(|p| p.to_path_buf());
|
|
let mut ig_err = Error::Io(io::Error::from(err));
|
|
if let Some(path) = path {
|
|
ig_err = Error::WithPath {
|
|
path: path,
|
|
err: Box::new(ig_err),
|
|
};
|
|
}
|
|
ig_err
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct PartialErrorBuilder(Vec<Error>);
|
|
|
|
impl PartialErrorBuilder {
|
|
fn push(&mut self, err: Error) {
|
|
self.0.push(err);
|
|
}
|
|
|
|
fn push_ignore_io(&mut self, err: Error) {
|
|
if !err.is_io() {
|
|
self.push(err);
|
|
}
|
|
}
|
|
|
|
fn maybe_push(&mut self, err: Option<Error>) {
|
|
if let Some(err) = err {
|
|
self.push(err);
|
|
}
|
|
}
|
|
|
|
fn maybe_push_ignore_io(&mut self, err: Option<Error>) {
|
|
if let Some(err) = err {
|
|
self.push_ignore_io(err);
|
|
}
|
|
}
|
|
|
|
fn into_error_option(mut self) -> Option<Error> {
|
|
if self.0.is_empty() {
|
|
None
|
|
} else if self.0.len() == 1 {
|
|
Some(self.0.pop().unwrap())
|
|
} else {
|
|
Some(Error::Partial(self.0))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The result of a glob match.
|
|
///
|
|
/// The type parameter `T` typically refers to a type that provides more
|
|
/// information about a particular match. For example, it might identify
|
|
/// the specific gitignore file and the specific glob pattern that caused
|
|
/// the match.
|
|
#[derive(Clone, Debug)]
|
|
pub enum Match<T> {
|
|
/// The path didn't match any glob.
|
|
None,
|
|
/// The highest precedent glob matched indicates the path should be
|
|
/// ignored.
|
|
Ignore(T),
|
|
/// The highest precedent glob matched indicates the path should be
|
|
/// whitelisted.
|
|
Whitelist(T),
|
|
}
|
|
|
|
impl<T> Match<T> {
|
|
/// Returns true if the match result didn't match any globs.
|
|
pub fn is_none(&self) -> bool {
|
|
match *self {
|
|
Match::None => true,
|
|
Match::Ignore(_) | Match::Whitelist(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Returns true if the match result implies the path should be ignored.
|
|
pub fn is_ignore(&self) -> bool {
|
|
match *self {
|
|
Match::Ignore(_) => true,
|
|
Match::None | Match::Whitelist(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Returns true if the match result implies the path should be
|
|
/// whitelisted.
|
|
pub fn is_whitelist(&self) -> bool {
|
|
match *self {
|
|
Match::Whitelist(_) => true,
|
|
Match::None | Match::Ignore(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Inverts the match so that `Ignore` becomes `Whitelist` and
|
|
/// `Whitelist` becomes `Ignore`. A non-match remains the same.
|
|
pub fn invert(self) -> Match<T> {
|
|
match self {
|
|
Match::None => Match::None,
|
|
Match::Ignore(t) => Match::Whitelist(t),
|
|
Match::Whitelist(t) => Match::Ignore(t),
|
|
}
|
|
}
|
|
|
|
/// Return the value inside this match if it exists.
|
|
pub fn inner(&self) -> Option<&T> {
|
|
match *self {
|
|
Match::None => None,
|
|
Match::Ignore(ref t) => Some(t),
|
|
Match::Whitelist(ref t) => Some(t),
|
|
}
|
|
}
|
|
|
|
/// Apply the given function to the value inside this match.
|
|
///
|
|
/// If the match has no value, then return the match unchanged.
|
|
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Match<U> {
|
|
match self {
|
|
Match::None => Match::None,
|
|
Match::Ignore(t) => Match::Ignore(f(t)),
|
|
Match::Whitelist(t) => Match::Whitelist(f(t)),
|
|
}
|
|
}
|
|
}
|