mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Wrap walkdir::DirEntry in a new type (#436)
* Add -follow support. * tests/find: Serialize find_time() find_time() relies on the working directory, but e.g. delete_on_dot_dir() will temporarily change directories, causing find_time() to fail when run in parallel. * find: Don't use uutils::error::set_exit_code() The global exit code can polute the results of other tests. Link: https://github.com/uutils/coreutils/issues/5777 * find: New WalkEntry wrapper The new type wraps DirEntry when possible, but also lets us pass a valid entry to matchers when walkdir returns a broken symlink error. It also implements a Metadata cache (part of #430). * find: Implement -H, -L, -P flags * find: Fix -follow -samefile * find: Fix -follow -newer * find: Implement -xtype * find: Fix -delete error handling --------- Co-authored-by: hanbings <hanbings@hanbings.io>
This commit is contained in:
co-authored by
hanbings
parent
465856ce24
commit
a7a73c325d
Generated
+1
-1
@@ -1130,4 +1130,4 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
@@ -5,9 +5,8 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use faccess::PathExt;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// Matcher for -{read,writ,execut}able.
|
||||
pub enum AccessMatcher {
|
||||
@@ -17,7 +16,7 @@ pub enum AccessMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for AccessMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path();
|
||||
|
||||
match self {
|
||||
|
||||
@@ -7,13 +7,10 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use std::fs::{self, FileType};
|
||||
use std::fs;
|
||||
use std::io::{self, stderr, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
pub struct DeleteMatcher;
|
||||
|
||||
@@ -22,17 +19,17 @@ impl DeleteMatcher {
|
||||
DeleteMatcher
|
||||
}
|
||||
|
||||
fn delete(&self, file_path: &Path, file_type: FileType) -> io::Result<()> {
|
||||
if file_type.is_dir() {
|
||||
fs::remove_dir(file_path)
|
||||
fn delete(&self, entry: &WalkEntry) -> io::Result<()> {
|
||||
if entry.file_type().is_dir() && !entry.path_is_symlink() {
|
||||
fs::remove_dir(entry.path())
|
||||
} else {
|
||||
fs::remove_file(file_path)
|
||||
fs::remove_file(entry.path())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for DeleteMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path();
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
@@ -43,9 +40,10 @@ impl Matcher for DeleteMatcher {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self.delete(path, file_info.file_type()) {
|
||||
match self.delete(file_info) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
matcher_io.set_exit_code(1);
|
||||
writeln!(&mut stderr(), "Failed to delete {path_str}: {e}").unwrap();
|
||||
false
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::Matcher;
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
pub struct EmptyMatcher;
|
||||
|
||||
@@ -20,7 +20,7 @@ impl EmptyMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for EmptyMatcher {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
if file_info.file_type().is_file() {
|
||||
match file_info.metadata() {
|
||||
Ok(meta) => meta.len() == 0,
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Paths encountered during a walk.
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fs::{self, Metadata};
|
||||
use std::io::{self, ErrorKind};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::Follow;
|
||||
|
||||
/// Wrapper for a directory entry.
|
||||
#[derive(Debug)]
|
||||
enum Entry {
|
||||
/// Wraps an explicit path and depth.
|
||||
Explicit(PathBuf, usize),
|
||||
/// Wraps a WalkDir entry.
|
||||
WalkDir(DirEntry),
|
||||
}
|
||||
|
||||
/// File types.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FileType {
|
||||
Unknown,
|
||||
Fifo,
|
||||
CharDevice,
|
||||
Directory,
|
||||
BlockDevice,
|
||||
Regular,
|
||||
Symlink,
|
||||
Socket,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn is_dir(self) -> bool {
|
||||
self == Self::Directory
|
||||
}
|
||||
|
||||
pub fn is_file(self) -> bool {
|
||||
self == Self::Regular
|
||||
}
|
||||
|
||||
pub fn is_symlink(self) -> bool {
|
||||
self == Self::Symlink
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fs::FileType> for FileType {
|
||||
fn from(t: fs::FileType) -> FileType {
|
||||
if t.is_dir() {
|
||||
return FileType::Directory;
|
||||
}
|
||||
if t.is_file() {
|
||||
return FileType::Regular;
|
||||
}
|
||||
if t.is_symlink() {
|
||||
return FileType::Symlink;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if t.is_fifo() {
|
||||
return FileType::Fifo;
|
||||
}
|
||||
if t.is_char_device() {
|
||||
return FileType::CharDevice;
|
||||
}
|
||||
if t.is_block_device() {
|
||||
return FileType::BlockDevice;
|
||||
}
|
||||
if t.is_socket() {
|
||||
return FileType::Socket;
|
||||
}
|
||||
}
|
||||
|
||||
FileType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// An error encountered while walking a file system.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WalkError {
|
||||
/// The path that caused the error, if known.
|
||||
path: Option<PathBuf>,
|
||||
/// The depth below the root path, if known.
|
||||
depth: Option<usize>,
|
||||
/// The io::Error::raw_os_error(), if known.
|
||||
raw: Option<i32>,
|
||||
}
|
||||
|
||||
impl WalkError {
|
||||
/// Get the path this error occurred on, if known.
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
|
||||
/// Get the traversal depth when this error occurred, if known.
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
self.depth
|
||||
}
|
||||
|
||||
/// Get the kind of I/O error.
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
io::Error::from(self).kind()
|
||||
}
|
||||
|
||||
/// Check for ErrorKind::{NotFound,NotADirectory}.
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
if self.kind() == ErrorKind::NotFound {
|
||||
return true;
|
||||
}
|
||||
|
||||
// NotADirectory is nightly-only
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if self.raw == Some(uucore::libc::ENOTDIR) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check for ErrorKind::FilesystemLoop.
|
||||
pub fn is_loop(&self) -> bool {
|
||||
#[cfg(unix)]
|
||||
return self.raw == Some(uucore::libc::ELOOP);
|
||||
|
||||
#[cfg(not(unix))]
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for WalkError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
let ioe = io::Error::from(self);
|
||||
if let Some(path) = &self.path {
|
||||
write!(f, "{}: {}", path.display(), ioe)
|
||||
} else {
|
||||
write!(f, "{}", ioe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WalkError {}
|
||||
|
||||
impl From<io::Error> for WalkError {
|
||||
fn from(e: io::Error) -> WalkError {
|
||||
WalkError::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&io::Error> for WalkError {
|
||||
fn from(e: &io::Error) -> WalkError {
|
||||
WalkError {
|
||||
path: None,
|
||||
depth: None,
|
||||
raw: e.raw_os_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<walkdir::Error> for WalkError {
|
||||
fn from(e: walkdir::Error) -> WalkError {
|
||||
WalkError::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&walkdir::Error> for WalkError {
|
||||
fn from(e: &walkdir::Error) -> WalkError {
|
||||
WalkError {
|
||||
path: e.path().map(|p| p.to_owned()),
|
||||
depth: Some(e.depth()),
|
||||
raw: e.io_error().and_then(|e| e.raw_os_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WalkError> for io::Error {
|
||||
fn from(e: WalkError) -> io::Error {
|
||||
io::Error::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WalkError> for io::Error {
|
||||
fn from(e: &WalkError) -> io::Error {
|
||||
e.raw
|
||||
.map(io::Error::from_raw_os_error)
|
||||
.unwrap_or_else(|| ErrorKind::Other.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A path encountered while walking a file system.
|
||||
#[derive(Debug)]
|
||||
pub struct WalkEntry {
|
||||
/// The wrapped path/dirent.
|
||||
inner: Entry,
|
||||
/// Whether to follow symlinks.
|
||||
follow: Follow,
|
||||
/// Cached metadata.
|
||||
meta: OnceCell<Result<Metadata, WalkError>>,
|
||||
}
|
||||
|
||||
impl WalkEntry {
|
||||
/// Create a new WalkEntry for a specific file.
|
||||
pub fn new(path: impl Into<PathBuf>, depth: usize, follow: Follow) -> Self {
|
||||
Self {
|
||||
inner: Entry::Explicit(path.into(), depth),
|
||||
follow,
|
||||
meta: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [walkdir::DirEntry] to a [WalkEntry]. Errors due to broken symbolic links will be
|
||||
/// converted to valid entries, but other errors will be propagated.
|
||||
pub fn from_walkdir(
|
||||
result: walkdir::Result<DirEntry>,
|
||||
follow: Follow,
|
||||
) -> Result<WalkEntry, WalkError> {
|
||||
let result = result.map_err(WalkError::from);
|
||||
|
||||
match result {
|
||||
Ok(entry) => {
|
||||
let ret = if entry.depth() == 0 && follow != Follow::Never {
|
||||
// DirEntry::file_type() is wrong for root symlinks when follow_root_links is set
|
||||
Self::new(entry.path(), 0, follow)
|
||||
} else {
|
||||
Self {
|
||||
inner: Entry::WalkDir(entry),
|
||||
follow,
|
||||
meta: OnceCell::new(),
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
Err(e) if e.is_not_found() => {
|
||||
// Detect broken symlinks and replace them with explicit entries
|
||||
if let (Some(path), Some(depth)) = (e.path(), e.depth()) {
|
||||
if let Ok(meta) = path.symlink_metadata() {
|
||||
return Ok(WalkEntry {
|
||||
inner: Entry::Explicit(path.into(), depth),
|
||||
follow: Follow::Never,
|
||||
meta: Ok(meta).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to this entry.
|
||||
pub fn path(&self) -> &Path {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => path.as_path(),
|
||||
Entry::WalkDir(ent) => ent.path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to this entry.
|
||||
pub fn into_path(self) -> PathBuf {
|
||||
match self.inner {
|
||||
Entry::Explicit(path, _) => path,
|
||||
Entry::WalkDir(ent) => ent.into_path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of this entry.
|
||||
pub fn file_name(&self) -> &OsStr {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => {
|
||||
// Path::file_name() only works if the last component is normal
|
||||
path.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.file_name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the depth of this entry below the root.
|
||||
pub fn depth(&self) -> usize {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, depth) => *depth,
|
||||
Entry::WalkDir(ent) => ent.depth(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get whether symbolic links are followed for this entry.
|
||||
pub fn follow(&self) -> bool {
|
||||
self.follow.follow_at_depth(self.depth())
|
||||
}
|
||||
|
||||
/// Get the metadata on a cache miss.
|
||||
fn get_metadata(&self) -> Result<Metadata, WalkError> {
|
||||
self.follow.metadata_at_depth(self.path(), self.depth())
|
||||
}
|
||||
|
||||
/// Get the [Metadata] for this entry, following symbolic links if appropriate.
|
||||
/// Multiple calls to this function will cache and re-use the same [Metadata].
|
||||
pub fn metadata(&self) -> Result<&Metadata, WalkError> {
|
||||
let result = self.meta.get_or_init(|| match &self.inner {
|
||||
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
|
||||
Entry::WalkDir(ent) => Ok(ent.metadata()?),
|
||||
});
|
||||
result.as_ref().map_err(|e| e.clone())
|
||||
}
|
||||
|
||||
/// Get the file type of this entry.
|
||||
pub fn file_type(&self) -> FileType {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, _) => self
|
||||
.metadata()
|
||||
.map(|m| m.file_type().into())
|
||||
.unwrap_or(FileType::Unknown),
|
||||
Entry::WalkDir(ent) => ent.file_type().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether this entry is a symbolic link, regardless of whether links
|
||||
/// are being followed.
|
||||
pub fn path_is_symlink(&self) -> bool {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => {
|
||||
if self.follow() {
|
||||
path.symlink_metadata()
|
||||
.is_ok_and(|m| m.file_type().is_symlink())
|
||||
} else {
|
||||
self.file_type().is_symlink()
|
||||
}
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.path_is_symlink(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,8 @@ use std::ffi::OsString;
|
||||
use std::io::{stderr, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
enum Arg {
|
||||
FileArg(Vec<OsString>),
|
||||
@@ -52,7 +51,7 @@ impl SingleExecMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for SingleExecMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let mut command = Command::new(&self.executable);
|
||||
let path_to_file = if self.exec_in_parent_dir {
|
||||
if let Some(f) = file_info.path().file_name() {
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::Matcher;
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// The latest mapping from dev_id to fs_type, used for saving mount info reads
|
||||
pub struct Cache {
|
||||
@@ -88,7 +88,7 @@ impl FileSystemMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for FileSystemMatcher {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
false
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use super::Matcher;
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::unistd::Group;
|
||||
@@ -57,8 +57,8 @@ impl GroupMatcher {
|
||||
|
||||
impl Matcher for GroupMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
let Ok(metadata) = file_info.path().metadata() else {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let Ok(metadata) = file_info.metadata() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Matcher for GroupMatcher {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
// The user group acquisition function for Windows systems is not implemented in MetadataExt,
|
||||
// so it is somewhat difficult to implement it. :(
|
||||
false
|
||||
@@ -82,14 +82,14 @@ pub struct NoGroupMatcher {}
|
||||
|
||||
impl Matcher for NoGroupMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
use nix::unistd::Gid;
|
||||
|
||||
if file_info.path().is_symlink() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(metadata) = file_info.path().metadata() else {
|
||||
let Ok(metadata) = file_info.metadata() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Matcher for NoGroupMatcher {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ mod tests {
|
||||
let foo_path = temp_dir.path().join("foo");
|
||||
let _ = File::create(foo_path).expect("create temp file");
|
||||
let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo");
|
||||
let file_gid = file_info.path().metadata().unwrap().gid();
|
||||
let file_gid = file_info.metadata().unwrap().gid();
|
||||
let file_group = Group::from_gid(Gid::from_raw(file_gid))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
|
||||
@@ -7,12 +7,10 @@
|
||||
use std::io::{stderr, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::glob::Pattern;
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
fn read_link_target(file_info: &DirEntry) -> Option<PathBuf> {
|
||||
fn read_link_target(file_info: &WalkEntry) -> Option<PathBuf> {
|
||||
match file_info.path().read_link() {
|
||||
Ok(target) => Some(target),
|
||||
Err(err) => {
|
||||
@@ -47,7 +45,7 @@ impl LinkNameMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for LinkNameMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
if let Some(target) = read_link_target(file_info) {
|
||||
self.pattern.matches(&target.to_string_lossy())
|
||||
} else {
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
//! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz").
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher contains a collection of other matchers. A file only matches
|
||||
/// if it matches ALL the contained sub-matchers. For sub-matchers that have
|
||||
@@ -33,7 +32,7 @@ impl Matcher for AndMatcher {
|
||||
/// Returns true if all sub-matchers return true. Short-circuiting does take
|
||||
/// place. If the nth sub-matcher returns false, then we immediately return
|
||||
/// and don't make any further calls.
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
for matcher in &self.submatchers {
|
||||
if !matcher.matches(dir_entry, matcher_io) {
|
||||
return false;
|
||||
@@ -109,7 +108,7 @@ impl Matcher for OrMatcher {
|
||||
/// Returns true if any sub-matcher returns true. Short-circuiting does take
|
||||
/// place. If the nth sub-matcher returns true, then we immediately return
|
||||
/// and don't make any further calls.
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
for matcher in &self.submatchers {
|
||||
if matcher.matches(dir_entry, matcher_io) {
|
||||
return true;
|
||||
@@ -206,7 +205,7 @@ impl ListMatcher {
|
||||
impl Matcher for ListMatcher {
|
||||
/// Calls matches on all submatcher objects, with no short-circuiting.
|
||||
/// Returns the result of the call to the final submatcher
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let mut rc = false;
|
||||
for matcher in &self.submatchers {
|
||||
rc = matcher.matches(dir_entry, matcher_io);
|
||||
@@ -311,7 +310,7 @@ impl ListMatcherBuilder {
|
||||
pub struct TrueMatcher;
|
||||
|
||||
impl Matcher for TrueMatcher {
|
||||
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -320,7 +319,7 @@ impl Matcher for TrueMatcher {
|
||||
pub struct FalseMatcher;
|
||||
|
||||
impl Matcher for FalseMatcher {
|
||||
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -339,7 +338,7 @@ impl NotMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for NotMatcher {
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
!self.submatcher.matches(dir_entry, matcher_io)
|
||||
}
|
||||
|
||||
@@ -370,7 +369,7 @@ mod tests {
|
||||
pub struct HasSideEffects;
|
||||
|
||||
impl Matcher for HasSideEffects {
|
||||
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -383,7 +382,7 @@ mod tests {
|
||||
struct Counter(Rc<RefCell<u32>>);
|
||||
|
||||
impl Matcher for Counter {
|
||||
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
*self.0.borrow_mut() += 1;
|
||||
true
|
||||
}
|
||||
|
||||
+135
-25
@@ -7,6 +7,7 @@
|
||||
mod access;
|
||||
mod delete;
|
||||
mod empty;
|
||||
mod entry;
|
||||
pub mod exec;
|
||||
pub mod fs;
|
||||
mod glob;
|
||||
@@ -32,11 +33,10 @@ mod user;
|
||||
use ::regex::Regex;
|
||||
use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
|
||||
use fs::FileSystemMatcher;
|
||||
use std::fs::File;
|
||||
use std::fs::{File, Metadata};
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
use std::{error::Error, str::FromStr};
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use self::access::AccessMatcher;
|
||||
use self::delete::DeleteMatcher;
|
||||
@@ -63,15 +63,80 @@ use self::time::{
|
||||
FileAgeRangeMatcher, FileTimeMatcher, FileTimeType, NewerMatcher, NewerOptionMatcher,
|
||||
NewerOptionType, NewerTimeMatcher,
|
||||
};
|
||||
use self::type_matcher::TypeMatcher;
|
||||
use self::type_matcher::{TypeMatcher, XtypeMatcher};
|
||||
use self::user::{NoUserMatcher, UserMatcher};
|
||||
|
||||
use super::{Config, Dependencies};
|
||||
|
||||
pub use entry::{FileType, WalkEntry, WalkError};
|
||||
|
||||
/// Symlink following mode.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Follow {
|
||||
/// Never follow symlinks (-P; default).
|
||||
Never,
|
||||
/// Follow symlinks on root paths only (-H).
|
||||
Roots,
|
||||
/// Always follow symlinks (-L).
|
||||
Always,
|
||||
}
|
||||
|
||||
impl Follow {
|
||||
/// Check whether to follow a path of the given depth.
|
||||
pub fn follow_at_depth(self, depth: usize) -> bool {
|
||||
match self {
|
||||
Follow::Never => false,
|
||||
Follow::Roots => depth == 0,
|
||||
Follow::Always => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get metadata for a [WalkEntry].
|
||||
pub fn metadata(self, entry: &WalkEntry) -> Result<Metadata, WalkError> {
|
||||
if self.follow_at_depth(entry.depth()) == entry.follow() {
|
||||
// Same follow flag, re-use cached metadata
|
||||
entry.metadata().cloned()
|
||||
} else if !entry.follow() && !entry.file_type().is_symlink() {
|
||||
// Not a symlink, re-use cached metadata
|
||||
entry.metadata().cloned()
|
||||
} else if entry.follow() && entry.file_type().is_symlink() {
|
||||
// Broken symlink, re-use cached metadata
|
||||
entry.metadata().cloned()
|
||||
} else {
|
||||
self.metadata_at_depth(entry.path(), entry.depth())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get metadata for a path from the command line.
|
||||
pub fn root_metadata(self, path: impl AsRef<Path>) -> Result<Metadata, WalkError> {
|
||||
self.metadata_at_depth(path, 0)
|
||||
}
|
||||
|
||||
/// Get metadata for a path, following symlinks as necessary.
|
||||
pub fn metadata_at_depth(
|
||||
self,
|
||||
path: impl AsRef<Path>,
|
||||
depth: usize,
|
||||
) -> Result<Metadata, WalkError> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if self.follow_at_depth(depth) {
|
||||
match path.metadata().map_err(WalkError::from) {
|
||||
Ok(meta) => return Ok(meta),
|
||||
Err(e) if !e.is_not_found() => return Err(e),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path.symlink_metadata()?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct holding references to outputs and any inputs that can't be derived
|
||||
/// from the file/directory info.
|
||||
pub struct MatcherIO<'a> {
|
||||
should_skip_dir: bool,
|
||||
exit_code: i32,
|
||||
quit: bool,
|
||||
deps: &'a dyn Dependencies,
|
||||
}
|
||||
@@ -79,9 +144,10 @@ pub struct MatcherIO<'a> {
|
||||
impl<'a> MatcherIO<'a> {
|
||||
pub fn new(deps: &dyn Dependencies) -> MatcherIO<'_> {
|
||||
MatcherIO {
|
||||
deps,
|
||||
should_skip_dir: false,
|
||||
exit_code: 0,
|
||||
quit: false,
|
||||
deps,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +160,15 @@ impl<'a> MatcherIO<'a> {
|
||||
self.should_skip_dir
|
||||
}
|
||||
|
||||
pub fn set_exit_code(&mut self, code: i32) {
|
||||
self.exit_code = code;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
self.exit_code
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.quit = true;
|
||||
}
|
||||
@@ -123,7 +198,7 @@ pub trait Matcher: 'static {
|
||||
}
|
||||
|
||||
/// Returns whether the given file matches the object's predicate.
|
||||
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool;
|
||||
fn matches(&self, entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool;
|
||||
|
||||
/// Returns whether the matcher has any side-effects (e.g. executing a
|
||||
/// command, deleting a file). Iff no such matcher exists in the chain, then
|
||||
@@ -149,8 +224,8 @@ impl Matcher for Box<dyn Matcher> {
|
||||
self
|
||||
}
|
||||
|
||||
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
(**self).matches(file_info, matcher_io)
|
||||
fn matches(&self, entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
(**self).matches(entry, matcher_io)
|
||||
}
|
||||
|
||||
fn has_side_effects(&self) -> bool {
|
||||
@@ -440,6 +515,13 @@ fn build_matcher_tree(
|
||||
i += 1;
|
||||
Some(TypeMatcher::new(args[i])?.into_box())
|
||||
}
|
||||
"-xtype" => {
|
||||
if i >= args.len() - 1 {
|
||||
return Err(From::from(format!("missing argument to {}", args[i])));
|
||||
}
|
||||
i += 1;
|
||||
Some(XtypeMatcher::new(args[i])?.into_box())
|
||||
}
|
||||
"-fstype" => {
|
||||
if i >= args.len() - 1 {
|
||||
return Err(From::from(format!("missing argument to {}", args[i])));
|
||||
@@ -457,7 +539,7 @@ fn build_matcher_tree(
|
||||
return Err(From::from(format!("missing argument to {}", args[i])));
|
||||
}
|
||||
i += 1;
|
||||
Some(NewerMatcher::new(args[i])?.into_box())
|
||||
Some(NewerMatcher::new(args[i], config.follow)?.into_box())
|
||||
}
|
||||
"-mtime" | "-atime" | "-ctime" => {
|
||||
if i >= args.len() - 1 {
|
||||
@@ -562,7 +644,8 @@ fn build_matcher_tree(
|
||||
}
|
||||
i += 1;
|
||||
let path = args[i];
|
||||
let matcher = SameFileMatcher::new(path).map_err(|e| format!("{path}: {e}"))?;
|
||||
let matcher = SameFileMatcher::new(path, config.follow)
|
||||
.map_err(|e| format!("{path}: {e}"))?;
|
||||
Some(matcher.into_box())
|
||||
}
|
||||
"-user" => {
|
||||
@@ -712,6 +795,21 @@ fn build_matcher_tree(
|
||||
|
||||
return Ok((i, top_level_matcher.build()));
|
||||
}
|
||||
"-follow" => {
|
||||
// This option affects multiple matchers.
|
||||
// 1. It will use noleaf by default. (but -noleaf No change of behavior)
|
||||
// Unless -L or -H is specified:
|
||||
// 2. changes the behaviour of the -newer predicate.
|
||||
// 3. consideration applies to -newerXY, -anewer and -cnewer
|
||||
// 4. -type predicate will always match against the type of
|
||||
// the file that a symbolic link points to rather than the link itself.
|
||||
//
|
||||
// 5. causes the -lname and -ilname predicates always to return false.
|
||||
// (unless they happen to match broken symbolic links)
|
||||
config.follow = Follow::Always;
|
||||
config.no_leaf_dirs = true;
|
||||
Some(TrueMatcher.into_box())
|
||||
}
|
||||
"-daystart" => {
|
||||
config.today_start = true;
|
||||
Some(TrueMatcher.into_box())
|
||||
@@ -825,25 +923,28 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::find::tests::fix_up_slashes;
|
||||
use crate::find::tests::FakeDependencies;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Helper function for tests to get a `DirEntry` object. directory should
|
||||
/// Helper function for tests to get a [WalkEntry] object. root should
|
||||
/// probably be a string starting with `test_data/` (cargo's tests run with
|
||||
/// a working directory set to the root findutils folder).
|
||||
pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry {
|
||||
for wrapped_dir_entry in WalkDir::new(fix_up_slashes(directory)) {
|
||||
let dir_entry = wrapped_dir_entry.unwrap();
|
||||
if dir_entry
|
||||
.path()
|
||||
.strip_prefix(directory)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
== fix_up_slashes(filename)
|
||||
{
|
||||
return dir_entry;
|
||||
}
|
||||
}
|
||||
panic!("Couldn't find {filename} in {directory}");
|
||||
pub fn get_dir_entry_for(root: &str, path: &str) -> WalkEntry {
|
||||
get_dir_entry_follow(root, path, Follow::Never)
|
||||
}
|
||||
|
||||
/// Get a [WalkEntry] with an explicit [Follow] flag.
|
||||
pub fn get_dir_entry_follow(root: &str, path: &str, follow: Follow) -> WalkEntry {
|
||||
let root = fix_up_slashes(root);
|
||||
let root = Path::new(&root);
|
||||
|
||||
let path = fix_up_slashes(path);
|
||||
let path = if path.is_empty() {
|
||||
root.to_owned()
|
||||
} else {
|
||||
root.join(path)
|
||||
};
|
||||
|
||||
let depth = path.components().count() - root.components().count();
|
||||
WalkEntry::new(path, depth, follow)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1204,6 +1305,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_top_level_matcher_follow_config() {
|
||||
let mut config = Config::default();
|
||||
|
||||
build_top_level_matcher(&["-follow"], &mut config).unwrap();
|
||||
|
||||
assert_eq!(config.follow, Follow::Always);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comparable_value_matches() {
|
||||
assert!(
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::glob::Pattern;
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher makes a comparison of the name against a shell wildcard
|
||||
/// pattern. See `glob::Pattern` for details on the exact syntax.
|
||||
@@ -23,7 +21,7 @@ impl NameMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for NameMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let name = file_info.file_name().to_string_lossy();
|
||||
self.pattern.matches(&name)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::glob::Pattern;
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher makes a comparison of the path against a shell wildcard
|
||||
/// pattern. See `glob::Pattern` for details on the exact syntax.
|
||||
@@ -23,7 +21,7 @@ impl PathMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for PathMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path().to_string_lossy();
|
||||
self.pattern.matches(&path)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ use std::error::Error;
|
||||
use std::io::{stderr, Write};
|
||||
#[cfg(unix)]
|
||||
use uucore::mode::{parse_numeric, parse_symbolic};
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[cfg(unix)]
|
||||
@@ -101,7 +100,7 @@ impl PermMatcher {
|
||||
|
||||
impl Matcher for PermMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
match file_info.metadata() {
|
||||
Ok(metadata) => {
|
||||
@@ -127,7 +126,7 @@ impl Matcher for PermMatcher {
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn matches(&self, _dummy_file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dummy_file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Permission matching not available on this platform!"
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{stderr, Write},
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::{stderr, Write};
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
pub enum PrintDelimiter {
|
||||
Newline,
|
||||
@@ -41,7 +37,7 @@ impl Printer {
|
||||
}
|
||||
}
|
||||
|
||||
fn print(&self, file_info: &DirEntry, mut out: impl Write, print_error_message: bool) {
|
||||
fn print(&self, file_info: &WalkEntry, mut out: impl Write, print_error_message: bool) {
|
||||
match write!(
|
||||
out,
|
||||
"{}{}",
|
||||
@@ -67,7 +63,7 @@ impl Printer {
|
||||
}
|
||||
|
||||
impl Matcher for Printer {
|
||||
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
if let Some(file) = &self.output_file {
|
||||
self.print(file_info, file, true);
|
||||
} else {
|
||||
@@ -86,7 +82,6 @@ impl Matcher for Printer {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::find::matchers::tests::get_dir_entry_for;
|
||||
|
||||
+29
-60
@@ -4,15 +4,18 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::{borrow::Cow, error::Error, fs, path::Path, time::SystemTime};
|
||||
use std::borrow::Cow;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use chrono::{format::StrftimeItems, DateTime, Local};
|
||||
use once_cell::unsync::OnceCell;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{FileType, Matcher, MatcherIO, WalkEntry, WalkError};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::{FileTypeExt, MetadataExt};
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
|
||||
const STANDARD_BLOCK_SIZE: u64 = 512;
|
||||
|
||||
@@ -340,7 +343,7 @@ impl FormatString {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_starting_point(file_info: &walkdir::DirEntry) -> &Path {
|
||||
fn get_starting_point(file_info: &WalkEntry) -> &Path {
|
||||
file_info
|
||||
.path()
|
||||
.ancestors()
|
||||
@@ -350,47 +353,23 @@ fn get_starting_point(file_info: &walkdir::DirEntry) -> &Path {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn format_non_link_file_type(file_type: fs::FileType) -> char {
|
||||
if file_type.is_file() {
|
||||
'f'
|
||||
} else if file_type.is_dir() {
|
||||
'd'
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
if file_type.is_block_device() {
|
||||
'b'
|
||||
} else if file_type.is_char_device() {
|
||||
'c'
|
||||
} else if file_type.is_fifo() {
|
||||
'p'
|
||||
} else if file_type.is_socket() {
|
||||
's'
|
||||
} else {
|
||||
'U'
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
'U'
|
||||
fn format_non_link_file_type(file_type: FileType) -> char {
|
||||
match file_type {
|
||||
FileType::Regular => 'f',
|
||||
FileType::Directory => 'd',
|
||||
FileType::BlockDevice => 'b',
|
||||
FileType::CharDevice => 'c',
|
||||
FileType::Fifo => 'p',
|
||||
FileType::Socket => 's',
|
||||
_ => 'U',
|
||||
}
|
||||
}
|
||||
|
||||
fn format_directive<'entry>(
|
||||
file_info: &'entry walkdir::DirEntry,
|
||||
file_info: &'entry WalkEntry,
|
||||
directive: &FormatDirective,
|
||||
meta_cell: &OnceCell<fs::Metadata>,
|
||||
) -> Result<Cow<'entry, str>, Box<dyn Error>> {
|
||||
let meta = || {
|
||||
meta_cell.get_or_try_init(|| {
|
||||
if file_info.path_is_symlink() && !file_info.file_type().is_symlink() {
|
||||
// The file_info already followed the symlink, meaning that the
|
||||
// metadata will be for the target file, which isn't the
|
||||
// behavior we want, so manually re-compute the metadata for the
|
||||
// symlink itself instead.
|
||||
file_info.path().symlink_metadata()
|
||||
} else {
|
||||
file_info.metadata().map_err(std::convert::Into::into)
|
||||
}
|
||||
})
|
||||
};
|
||||
let meta = || file_info.metadata();
|
||||
|
||||
// NOTE ON QUOTING:
|
||||
// GNU find's man page claims that several directives that print names (like
|
||||
@@ -556,17 +535,10 @@ fn format_directive<'entry>(
|
||||
|
||||
FormatDirective::Type { follow_links } => if file_info.path_is_symlink() {
|
||||
if *follow_links {
|
||||
match file_info.path().metadata() {
|
||||
Ok(meta) => format_non_link_file_type(meta.file_type()),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => 'N',
|
||||
// The ErrorKinds corresponding to ELOOP and ENOTDIR are
|
||||
// nightly-only:
|
||||
// https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.FilesystemLoop
|
||||
// so we need to use the raw errno values instead.
|
||||
#[cfg(unix)]
|
||||
Err(e) if e.raw_os_error().unwrap_or(0) == uucore::libc::ENOTDIR => 'N',
|
||||
#[cfg(unix)]
|
||||
Err(e) if e.raw_os_error().unwrap_or(0) == uucore::libc::ELOOP => 'L',
|
||||
match file_info.path().metadata().map_err(WalkError::from) {
|
||||
Ok(meta) => format_non_link_file_type(meta.file_type().into()),
|
||||
Err(e) if e.is_not_found() => 'N',
|
||||
Err(e) if e.is_loop() => 'L',
|
||||
Err(_) => '?',
|
||||
}
|
||||
} else {
|
||||
@@ -610,11 +582,8 @@ impl Printf {
|
||||
}
|
||||
|
||||
impl Matcher for Printf {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let mut out = matcher_io.deps.get_output().borrow_mut();
|
||||
// The metadata is computed lazily, so that anything being printed
|
||||
// without needing metadata won't incur any performance overhead.
|
||||
let meta_cell = OnceCell::new();
|
||||
|
||||
for component in &self.format.components {
|
||||
match component {
|
||||
@@ -624,7 +593,7 @@ impl Matcher for Printf {
|
||||
directive,
|
||||
width,
|
||||
justify,
|
||||
} => match format_directive(file_info, directive, &meta_cell) {
|
||||
} => match format_directive(file_info, directive) {
|
||||
Ok(content) => {
|
||||
if let Some(width) = width {
|
||||
match justify {
|
||||
@@ -1110,13 +1079,13 @@ mod tests {
|
||||
let new_file_name = "newFile";
|
||||
let file = File::create(temp_dir.path().join(new_file_name)).expect("create temp file");
|
||||
|
||||
let file_info = get_dir_entry_for(&temp_dir_path, new_file_name);
|
||||
let deps = FakeDependencies::new();
|
||||
|
||||
let mut perms = file_info.metadata().unwrap().permissions();
|
||||
let mut perms = file.metadata().unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
file.set_permissions(perms).unwrap();
|
||||
|
||||
let file_info = get_dir_entry_for(&temp_dir_path, new_file_name);
|
||||
let deps = FakeDependencies::new();
|
||||
|
||||
let matcher = Printf::new("%m %M").unwrap();
|
||||
assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
|
||||
assert_eq!("755 -rwxr-xr-x", deps.get_output_as_string());
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher checks the type of the file.
|
||||
pub struct PruneMatcher;
|
||||
@@ -18,7 +16,7 @@ impl PruneMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for PruneMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
if file_info.file_type().is_dir() {
|
||||
matcher_io.mark_current_dir_to_be_skipped();
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher quits the search immediately.
|
||||
pub struct QuitMatcher;
|
||||
|
||||
impl Matcher for QuitMatcher {
|
||||
fn matches(&self, _: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
matcher_io.quit();
|
||||
true
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{error::Error, fmt, str::FromStr};
|
||||
|
||||
use onig::{Regex, RegexOptions, Syntax};
|
||||
|
||||
use super::Matcher;
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseRegexTypeError(String);
|
||||
@@ -111,7 +111,7 @@ impl RegexMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for RegexMatcher {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
self.regex
|
||||
.is_match(file_info.path().to_string_lossy().as_ref())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use super::Matcher;
|
||||
use super::{Follow, Matcher, MatcherIO, WalkEntry, WalkError};
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use uucore::fs::FileInformation;
|
||||
@@ -12,16 +12,32 @@ pub struct SameFileMatcher {
|
||||
info: FileInformation,
|
||||
}
|
||||
|
||||
/// Gets FileInformation, possibly following symlinks, but falling back on
|
||||
/// broken links.
|
||||
fn get_file_info(path: &Path, follow: bool) -> Result<FileInformation, WalkError> {
|
||||
if follow {
|
||||
let result = FileInformation::from_path(path, true).map_err(WalkError::from);
|
||||
|
||||
match result {
|
||||
Ok(info) => return Ok(info),
|
||||
Err(e) if !e.is_not_found() => return Err(e),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FileInformation::from_path(path, false)?)
|
||||
}
|
||||
|
||||
impl SameFileMatcher {
|
||||
pub fn new(path: impl AsRef<Path>) -> Result<Self, Box<dyn Error>> {
|
||||
let info = FileInformation::from_path(path, false)?;
|
||||
pub fn new(path: impl AsRef<Path>, follow: Follow) -> Result<Self, Box<dyn Error>> {
|
||||
let info = get_file_info(path.as_ref(), follow != Follow::Never)?;
|
||||
Ok(Self { info })
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for SameFileMatcher {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _matcher_io: &mut super::MatcherIO) -> bool {
|
||||
if let Ok(info) = FileInformation::from_path(file_info.path(), false) {
|
||||
fn matches(&self, file_info: &WalkEntry, _matcher_io: &mut MatcherIO) -> bool {
|
||||
if let Ok(info) = get_file_info(file_info.path(), file_info.follow()) {
|
||||
info == self.info
|
||||
} else {
|
||||
false
|
||||
@@ -31,29 +47,61 @@ impl Matcher for SameFileMatcher {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use super::*;
|
||||
|
||||
use crate::find::matchers::tests::{get_dir_entry_follow, get_dir_entry_for};
|
||||
use crate::find::tests::FakeDependencies;
|
||||
use std::fs::{self, File};
|
||||
use tempfile::Builder;
|
||||
|
||||
#[test]
|
||||
fn test_samefile() {
|
||||
use crate::find::{
|
||||
matchers::{samefile::SameFileMatcher, tests::get_dir_entry_for, Matcher},
|
||||
tests::FakeDependencies,
|
||||
};
|
||||
let root = Builder::new().prefix("example").tempdir().unwrap();
|
||||
let root_path = root.path();
|
||||
|
||||
// remove file if hard link file exist.
|
||||
// But you can't delete a file that doesn't exist,
|
||||
// so ignore the error returned here.
|
||||
let _ = fs::remove_file("test_data/links/hard_link");
|
||||
let file_path = root_path.join("file");
|
||||
File::create(&file_path).unwrap();
|
||||
|
||||
assert!(SameFileMatcher::new("test_data/links/hard_link").is_err());
|
||||
let link_path = root_path.join("link");
|
||||
fs::hard_link(&file_path, &link_path).unwrap();
|
||||
|
||||
fs::hard_link("test_data/links/abbbc", "test_data/links/hard_link").unwrap();
|
||||
let other_path = root_path.join("other");
|
||||
File::create(&other_path).unwrap();
|
||||
|
||||
let file = get_dir_entry_for("test_data/links", "abbbc");
|
||||
let hard_link_file = get_dir_entry_for("test_data/links", "hard_link");
|
||||
let matcher = SameFileMatcher::new(file.into_path()).unwrap();
|
||||
let matcher = SameFileMatcher::new(&file_path, Follow::Never).unwrap();
|
||||
|
||||
let root_path = root_path.to_string_lossy();
|
||||
let file_entry = get_dir_entry_for(&root_path, "file");
|
||||
let link_entry = get_dir_entry_for(&root_path, "link");
|
||||
let other_entry = get_dir_entry_for(&root_path, "other");
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
assert!(matcher.matches(&hard_link_file, &mut deps.new_matcher_io()));
|
||||
assert!(matcher.matches(&file_entry, &mut deps.new_matcher_io()));
|
||||
assert!(matcher.matches(&link_entry, &mut deps.new_matcher_io()));
|
||||
assert!(!matcher.matches(&other_entry, &mut deps.new_matcher_io()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_follow() {
|
||||
let deps = FakeDependencies::new();
|
||||
let matcher = SameFileMatcher::new("test_data/links/link-f", Follow::Roots).unwrap();
|
||||
|
||||
let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Never);
|
||||
assert!(!matcher.matches(&entry, &mut deps.new_matcher_io()));
|
||||
|
||||
let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Never);
|
||||
assert!(matcher.matches(&entry, &mut deps.new_matcher_io()));
|
||||
|
||||
let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Roots);
|
||||
assert!(!matcher.matches(&entry, &mut deps.new_matcher_io()));
|
||||
|
||||
let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Roots);
|
||||
assert!(matcher.matches(&entry, &mut deps.new_matcher_io()));
|
||||
|
||||
let entry = get_dir_entry_follow("test_data/links", "link-f", Follow::Always);
|
||||
assert!(matcher.matches(&entry, &mut deps.new_matcher_io()));
|
||||
|
||||
let entry = get_dir_entry_follow("test_data/links", "abbbc", Follow::Always);
|
||||
assert!(matcher.matches(&entry, &mut deps.new_matcher_io()));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user