PageRenderTime 32ms CodeModel.GetById 7ms RepoModel.GetById 1ms app.codeStats 0ms

/crates/core/subject.rs

https://github.com/BurntSushi/ripgrep
Rust | 160 lines | 93 code | 14 blank | 53 comment | 13 complexity | 5652dca2580d3b84d0c8fbabeaebbdd5 MD5 | raw file
Possible License(s): MIT, Unlicense
  1. use std::path::Path;
  2. use ignore::{self, DirEntry};
  3. use log;
  4. /// A configuration for describing how subjects should be built.
  5. #[derive(Clone, Debug)]
  6. struct Config {
  7. strip_dot_prefix: bool,
  8. }
  9. impl Default for Config {
  10. fn default() -> Config {
  11. Config { strip_dot_prefix: false }
  12. }
  13. }
  14. /// A builder for constructing things to search over.
  15. #[derive(Clone, Debug)]
  16. pub struct SubjectBuilder {
  17. config: Config,
  18. }
  19. impl SubjectBuilder {
  20. /// Return a new subject builder with a default configuration.
  21. pub fn new() -> SubjectBuilder {
  22. SubjectBuilder { config: Config::default() }
  23. }
  24. /// Create a new subject from a possibly missing directory entry.
  25. ///
  26. /// If the directory entry isn't present, then the corresponding error is
  27. /// logged if messages have been configured. Otherwise, if the subject is
  28. /// deemed searchable, then it is returned.
  29. pub fn build_from_result(
  30. &self,
  31. result: Result<DirEntry, ignore::Error>,
  32. ) -> Option<Subject> {
  33. match result {
  34. Ok(dent) => self.build(dent),
  35. Err(err) => {
  36. err_message!("{}", err);
  37. None
  38. }
  39. }
  40. }
  41. /// Create a new subject using this builder's configuration.
  42. ///
  43. /// If a subject could not be created or should otherwise not be searched,
  44. /// then this returns `None` after emitting any relevant log messages.
  45. pub fn build(&self, dent: DirEntry) -> Option<Subject> {
  46. let subj =
  47. Subject { dent, strip_dot_prefix: self.config.strip_dot_prefix };
  48. if let Some(ignore_err) = subj.dent.error() {
  49. ignore_message!("{}", ignore_err);
  50. }
  51. // If this entry was explicitly provided by an end user, then we always
  52. // want to search it.
  53. if subj.is_explicit() {
  54. return Some(subj);
  55. }
  56. // At this point, we only want to search something if it's explicitly a
  57. // file. This omits symlinks. (If ripgrep was configured to follow
  58. // symlinks, then they have already been followed by the directory
  59. // traversal.)
  60. if subj.is_file() {
  61. return Some(subj);
  62. }
  63. // We got nothin. Emit a debug message, but only if this isn't a
  64. // directory. Otherwise, emitting messages for directories is just
  65. // noisy.
  66. if !subj.is_dir() {
  67. log::debug!(
  68. "ignoring {}: failed to pass subject filter: \
  69. file type: {:?}, metadata: {:?}",
  70. subj.dent.path().display(),
  71. subj.dent.file_type(),
  72. subj.dent.metadata()
  73. );
  74. }
  75. None
  76. }
  77. /// When enabled, if the subject's file path starts with `./` then it is
  78. /// stripped.
  79. ///
  80. /// This is useful when implicitly searching the current working directory.
  81. pub fn strip_dot_prefix(&mut self, yes: bool) -> &mut SubjectBuilder {
  82. self.config.strip_dot_prefix = yes;
  83. self
  84. }
  85. }
  86. /// A subject is a thing we want to search. Generally, a subject is either a
  87. /// file or stdin.
  88. #[derive(Clone, Debug)]
  89. pub struct Subject {
  90. dent: DirEntry,
  91. strip_dot_prefix: bool,
  92. }
  93. impl Subject {
  94. /// Return the file path corresponding to this subject.
  95. ///
  96. /// If this subject corresponds to stdin, then a special `<stdin>` path
  97. /// is returned instead.
  98. pub fn path(&self) -> &Path {
  99. if self.strip_dot_prefix && self.dent.path().starts_with("./") {
  100. self.dent.path().strip_prefix("./").unwrap()
  101. } else {
  102. self.dent.path()
  103. }
  104. }
  105. /// Returns true if and only if this entry corresponds to stdin.
  106. pub fn is_stdin(&self) -> bool {
  107. self.dent.is_stdin()
  108. }
  109. /// Returns true if and only if this entry corresponds to a subject to
  110. /// search that was explicitly supplied by an end user.
  111. ///
  112. /// Generally, this corresponds to either stdin or an explicit file path
  113. /// argument. e.g., in `rg foo some-file ./some-dir/`, `some-file` is
  114. /// an explicit subject, but, e.g., `./some-dir/some-other-file` is not.
  115. ///
  116. /// However, note that ripgrep does not see through shell globbing. e.g.,
  117. /// in `rg foo ./some-dir/*`, `./some-dir/some-other-file` will be treated
  118. /// as an explicit subject.
  119. pub fn is_explicit(&self) -> bool {
  120. // stdin is obvious. When an entry has a depth of 0, that means it
  121. // was explicitly provided to our directory iterator, which means it
  122. // was in turn explicitly provided by the end user. The !is_dir check
  123. // means that we want to search files even if their symlinks, again,
  124. // because they were explicitly provided. (And we never want to try
  125. // to search a directory.)
  126. self.is_stdin() || (self.dent.depth() == 0 && !self.is_dir())
  127. }
  128. /// Returns true if and only if this subject points to a directory after
  129. /// following symbolic links.
  130. fn is_dir(&self) -> bool {
  131. let ft = match self.dent.file_type() {
  132. None => return false,
  133. Some(ft) => ft,
  134. };
  135. if ft.is_dir() {
  136. return true;
  137. }
  138. // If this is a symlink, then we want to follow it to determine
  139. // whether it's a directory or not.
  140. self.dent.path_is_symlink() && self.dent.path().is_dir()
  141. }
  142. /// Returns true if and only if this subject points to a file.
  143. fn is_file(&self) -> bool {
  144. self.dent.file_type().map_or(false, |ft| ft.is_file())
  145. }
  146. }