diff options
-rw-r--r-- | ignore/src/walk.rs | 118 |
1 files changed, 110 insertions, 8 deletions
diff --git a/ignore/src/walk.rs b/ignore/src/walk.rs index 8feb4741..2f54b53f 100644 --- a/ignore/src/walk.rs +++ b/ignore/src/walk.rs @@ -455,6 +455,7 @@ pub struct WalkBuilder { same_file_system: bool, sorter: Option<Sorter>, threads: usize, + skip: Option<Arc<Handle>>, } #[derive(Clone)] @@ -472,6 +473,7 @@ impl fmt::Debug for WalkBuilder { .field("max_filesize", &self.max_filesize) .field("follow_links", &self.follow_links) .field("threads", &self.threads) + .field("skip", &self.skip) .finish() } } @@ -493,6 +495,7 @@ impl WalkBuilder { same_file_system: false, sorter: None, threads: 0, + skip: None, } } @@ -535,6 +538,7 @@ impl WalkBuilder { ig_root: ig_root.clone(), ig: ig_root.clone(), max_filesize: self.max_filesize, + skip: self.skip.clone(), } } @@ -552,6 +556,7 @@ impl WalkBuilder { follow_links: self.follow_links, same_file_system: self.same_file_system, threads: self.threads, + skip: self.skip.clone(), } } @@ -792,6 +797,26 @@ impl WalkBuilder { self.same_file_system = yes; self } + + /// Do not yield directory entries that are believed to correspond to + /// stdout. + /// + /// This is useful when a command is invoked via shell redirection to a + /// file that is also being read. For example, `grep -r foo ./ > results` + /// might end up trying to search `results` even though it is also writing + /// to it, which could cause an unbounded feedback loop. Setting this + /// option prevents this from happening by skipping over the `results` + /// file. + /// + /// This is disabled by default. + pub fn skip_stdout(&mut self, yes: bool) -> &mut WalkBuilder { + if yes { + self.skip = stdout_handle().map(Arc::new); + } else { + self.skip = None; + } + self + } } /// Walk is a recursive directory iterator over file paths in one or more @@ -806,6 +831,7 @@ pub struct Walk { ig_root: Ignore, ig: Ignore, max_filesize: Option<u64>, + skip: Option<Arc<Handle>>, } impl Walk { @@ -818,12 +844,17 @@ impl Walk { WalkBuilder::new(path).build() } - fn skip_entry(&self, ent: &walkdir::DirEntry) -> bool { + fn skip_entry(&self, ent: &DirEntry) -> Result<bool, Error> { if ent.depth() == 0 { - return false; + return Ok(false); } - let is_dir = ent.file_type().is_dir(); + if let Some(ref stdout) = self.skip { + if path_equals(ent, stdout)? { + return Ok(true); + } + } + let is_dir = ent.file_type().map_or(false, |ft| ft.is_dir()); let max_size = self.max_filesize; let should_skip_path = skip_path(&self.ig, ent.path(), is_dir); let should_skip_filesize = if !is_dir && max_size.is_some() { @@ -832,7 +863,7 @@ impl Walk { false }; - should_skip_path || should_skip_filesize + Ok(should_skip_path || should_skip_filesize) } } @@ -874,7 +905,12 @@ impl Iterator for Walk { self.ig = self.ig.parent().unwrap(); } Ok(WalkEvent::Dir(ent)) => { - if self.skip_entry(&ent) { + let mut ent = DirEntry::new_walkdir(ent, None); + let should_skip = match self.skip_entry(&ent) { + Err(err) => return Some(Err(err)), + Ok(should_skip) => should_skip, + }; + if should_skip { self.it.as_mut().unwrap().it.skip_current_dir(); // Still need to push this on the stack because // we'll get a WalkEvent::Exit event for this dir. @@ -885,13 +921,19 @@ impl Iterator for Walk { } let (igtmp, err) = self.ig.add_child(ent.path()); self.ig = igtmp; - return Some(Ok(DirEntry::new_walkdir(ent, err))); + ent.err = err; + return Some(Ok(ent)); } Ok(WalkEvent::File(ent)) => { - if self.skip_entry(&ent) { + let ent = DirEntry::new_walkdir(ent, None); + let should_skip = match self.skip_entry(&ent) { + Err(err) => return Some(Err(err)), + Ok(should_skip) => should_skip, + }; + if should_skip { continue; } - return Some(Ok(DirEntry::new_walkdir(ent, None))); + return Some(Ok(ent)); } } } @@ -993,6 +1035,7 @@ pub struct WalkParallel { follow_links: bool, same_file_system: bool, threads: usize, + skip: Option<Arc<Handle>>, } impl WalkParallel { @@ -1080,6 +1123,7 @@ impl WalkParallel { max_depth: self.max_depth, max_filesize: self.max_filesize, follow_links: self.follow_links, + skip: self.skip.clone(), }; handles.push(thread::spawn(|| worker.run())); } @@ -1208,6 +1252,9 @@ struct Worker { /// Whether to follow symbolic links or not. When this is enabled, loop /// detection is performed. follow_links: bool, + /// A file handle to skip, currently is either `None` or stdout, if it's + /// a file and it has been requested to skip files identical to stdout. + skip: Option<Arc<Handle>>, } impl Worker { @@ -1335,6 +1382,15 @@ impl Worker { } } } + if let Some(ref stdout) = self.skip { + let is_stdout = match path_equals(&dent, stdout) { + Ok(is_stdout) => is_stdout, + Err(err) => return (self.f)(Err(err)), + }; + if is_stdout { + return WalkState::Continue; + } + } let is_dir = dent.is_dir(); let max_size = self.max_filesize; let should_skip_path = skip_path(ig, dent.path(), is_dir); @@ -1541,6 +1597,52 @@ fn skip_path( } } +/// Returns a handle to stdout for filtering search. +/// +/// A handle is returned if and only if stdout is being redirected to a file. +/// The handle returned corresponds to that file. +/// +/// This can be used to ensure that we do not attempt to search a file that we +/// may also be writing to. +fn stdout_handle() -> Option<Handle> { + let h = match Handle::stdout() { + Err(_) => return None, + Ok(h) => h, + }; + let md = match h.as_file().metadata() { + Err(_) => return None, + Ok(md) => md, + }; + if !md.is_file() { + return None; + } + Some(h) +} + +/// Returns true if and only if the given directory entry is believed to be +/// equivalent to the given handle. If there was a problem querying the path +/// for information to determine equality, then that error is returned. +fn path_equals(dent: &DirEntry, handle: &Handle) -> Result<bool, Error> { + #[cfg(unix)] + fn never_equal(dent: &DirEntry, handle: &Handle) -> bool { + dent.ino() != Some(handle.ino()) + } + + #[cfg(not(unix))] + fn never_equal(_: &DirEntry, _: &Handle) -> bool { + false + } + + // If we know for sure that these two things aren't equal, then avoid + // the costly extra stat call to determine equality. + if dent.is_stdin() || never_equal(dent, handle) { + return Ok(false); + } + Handle::from_path(dent.path()) + .map(|h| &h == handle) + .map_err(|err| Error::Io(err).with_path(dent.path())) +} + /// Returns true if and only if the given path is on the same device as the /// given root device. fn is_same_file_system(root_device: u64, path: &Path) -> Result<bool, Error> { |