use std::path::Path; use ignore::{self, DirEntry}; use log; /// A configuration for describing how subjects should be built. #[derive(Clone, Debug)] struct Config { strip_dot_prefix: bool, } impl Default for Config { fn default() -> Config { Config { strip_dot_prefix: false, } } } /// A builder for constructing things to search over. #[derive(Clone, Debug)] pub struct SubjectBuilder { config: Config, } impl SubjectBuilder { /// Return a new subject builder with a default configuration. pub fn new() -> SubjectBuilder { SubjectBuilder { config: Config::default() } } /// Create a new subject from a possibly missing directory entry. /// /// If the directory entry isn't present, then the corresponding error is /// logged if messages have been configured. Otherwise, if the subject is /// deemed searchable, then it is returned. pub fn build_from_result( &self, result: Result, ) -> Option { match result { Ok(dent) => self.build(dent), Err(err) => { err_message!("{}", err); None } } } /// Create a new subject using this builder's configuration. /// /// If a subject could not be created or should otherwise not be searched, /// then this returns `None` after emitting any relevant log messages. pub fn build(&self, dent: DirEntry) -> Option { let subj = Subject { dent: dent, strip_dot_prefix: self.config.strip_dot_prefix, }; if let Some(ignore_err) = subj.dent.error() { ignore_message!("{}", ignore_err); } // If this entry was explicitly provided by an end user, then we always // want to search it. if subj.is_explicit() { return Some(subj); } // At this point, we only want to search something if it's explicitly a // file. This omits symlinks. (If ripgrep was configured to follow // symlinks, then they have already been followed by the directory // traversal.) if subj.is_file() { return Some(subj); } // We got nothin. Emit a debug message, but only if this isn't a // directory. Otherwise, emitting messages for directories is just // noisy. if !subj.is_dir() { log::debug!( "ignoring {}: failed to pass subject filter: \ file type: {:?}, metadata: {:?}", subj.dent.path().display(), subj.dent.file_type(), subj.dent.metadata() ); } None } /// When enabled, if the subject's file path starts with `./` then it is /// stripped. /// /// This is useful when implicitly searching the current working directory. pub fn strip_dot_prefix(&mut self, yes: bool) -> &mut SubjectBuilder { self.config.strip_dot_prefix = yes; self } } /// A subject is a thing we want to search. Generally, a subject is either a /// file or stdin. #[derive(Clone, Debug)] pub struct Subject { dent: DirEntry, strip_dot_prefix: bool, } impl Subject { /// Return the file path corresponding to this subject. /// /// If this subject corresponds to stdin, then a special `` path /// is returned instead. pub fn path(&self) -> &Path { if self.strip_dot_prefix && self.dent.path().starts_with("./") { self.dent.path().strip_prefix("./").unwrap() } else { self.dent.path() } } /// Returns true if and only if this entry corresponds to stdin. pub fn is_stdin(&self) -> bool { self.dent.is_stdin() } /// Returns true if and only if this entry corresponds to a subject to /// search that was explicitly supplied by an end user. /// /// Generally, this corresponds to either stdin or an explicit file path /// argument. e.g., in `rg foo some-file ./some-dir/`, `some-file` is /// an explicit subject, but, e.g., `./some-dir/some-other-file` is not. /// /// However, note that ripgrep does not see through shell globbing. e.g., /// in `rg foo ./some-dir/*`, `./some-dir/some-other-file` will be treated /// as an explicit subject. pub fn is_explicit(&self) -> bool { // stdin is obvious. When an entry has a depth of 0, that means it // was explicitly provided to our directory iterator, which means it // was in turn explicitly provided by the end user. The !is_dir check // means that we want to search files even if their symlinks, again, // because they were explicitly provided. (And we never want to try // to search a directory.) self.is_stdin() || (self.dent.depth() == 0 && !self.is_dir()) } /// Returns true if and only if this subject points to a directory after /// following symbolic links. fn is_dir(&self) -> bool { let ft = match self.dent.file_type() { None => return false, Some(ft) => ft, }; if ft.is_dir() { return true; } // If this is a symlink, then we want to follow it to determine // whether it's a directory or not. self.dent.path_is_symlink() && self.dent.path().is_dir() } /// Returns true if and only if this subject points to a file. fn is_file(&self) -> bool { self.dent.file_type().map_or(false, |ft| ft.is_file()) } }