From aa70c5a446ae922de4e4cd3fdbd3c91cd43fa765 Mon Sep 17 00:00:00 2001 From: sharkdp Date: Sun, 19 Aug 2018 17:05:04 +0200 Subject: [PATCH] Add `--type empty` Add a new `empty`/`e` type to search for empty files and/or directories. To search for both empty files and directories, use one of the following: fd --type empty fd -te fd --type empty --type file --type directory To search for empty files, use fd --type empty --type file fd -te -tf To search for empty directories, use fd --type empty --type directory fd -te -td closes #273 --- doc/fd.1 | 2 ++ src/app.rs | 7 +++++-- src/fshelper/mod.rs | 22 +++++++++++++++++++++- src/internal.rs | 2 ++ src/main.rs | 10 ++++++++++ src/walk.rs | 1 + tests/tests.rs | 27 +++++++++++++++++++++++++++ 7 files changed, 68 insertions(+), 3 deletions(-) diff --git a/doc/fd.1 b/doc/fd.1 index 450bdad..3099a91 100644 --- a/doc/fd.1 +++ b/doc/fd.1 @@ -86,6 +86,8 @@ directories symbolic links .IP "x, executable" executable (files) +.IP "e, empty" +empty files or directories .RE .RS diff --git a/src/app.rs b/src/app.rs index e79e2a1..7496b5c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -87,6 +87,8 @@ pub fn build_app() -> App<'static, 'static> { "symlink", "x", "executable", + "e", + "empty", ]).hide_possible_values(true), ).arg( arg("extension") @@ -196,12 +198,13 @@ fn usage() -> HashMap<&'static str, Help> { , "Limit the directory traversal to a given depth. By default, there is no limit \ on the search depth."); doc!(h, "file-type" - , "Filter by type: file (f), directory (d), symlink (l),\nexecutable (x)" + , "Filter by type: file (f), directory (d), symlink (l),\nexecutable (x), empty (e)" , "Filter the search by type (multiple allowable filetypes can be specified):\n \ 'f' or 'file': regular files\n \ 'd' or 'directory': directories\n \ 'l' or 'symlink': symbolic links\n \ - 'x' or 'executable': executables"); + 'x' or 'executable': executables\n \ + 'e' or 'empty': empty files or directories"); doc!(h, "extension" , "Filter by file extension" , "(Additionally) filter search results by their file extension. Multiple allowable file \ diff --git a/src/fshelper/mod.rs b/src/fshelper/mod.rs index 37677e6..33f6fd7 100644 --- a/src/fshelper/mod.rs +++ b/src/fshelper/mod.rs @@ -13,6 +13,8 @@ use std::io; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; +use ignore::DirEntry; + pub fn path_absolute_form(path: &Path) -> io::Result { if path.is_absolute() { Ok(path.to_path_buf()) @@ -36,7 +38,7 @@ pub fn absolute_path(path: &Path) -> io::Result { Ok(path_buf) } -// Path::is_dir() is not guarandteed to be intuitively correct for "." and ".." +// Path::is_dir() is not guaranteed to be intuitively correct for "." and ".." // See: https://github.com/rust-lang/rust/issues/45302 pub fn is_dir(path: &Path) -> bool { if path.file_name().is_some() { @@ -55,3 +57,21 @@ pub fn is_executable(md: &fs::Metadata) -> bool { pub fn is_executable(_: &fs::Metadata) -> bool { false } + +pub fn is_empty(entry: &DirEntry) -> bool { + if let Some(file_type) = entry.file_type() { + if file_type.is_dir() { + if let Ok(mut entries) = fs::read_dir(entry.path()) { + entries.next().is_none() + } else { + false + } + } else if file_type.is_file() { + entry.metadata().map(|m| m.len() == 0).unwrap_or(false) + } else { + false + } + } else { + false + } +} diff --git a/src/internal.rs b/src/internal.rs index 5bffb7a..263571d 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -28,6 +28,7 @@ pub struct FileTypes { pub directories: bool, pub symlinks: bool, pub executables_only: bool, + pub empty_only: bool, } impl Default for FileTypes { @@ -37,6 +38,7 @@ impl Default for FileTypes { directories: false, symlinks: false, executables_only: false, + empty_only: false, } } } diff --git a/src/main.rs b/src/main.rs index 9f2c4a1..0de41f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,9 +182,19 @@ fn main() { file_types.executables_only = true; file_types.files = true; } + "e" | "empty" => { + file_types.empty_only = true; + } _ => unreachable!(), } } + + // If only 'empty' was specified, search for both files and directories: + if file_types.empty_only && !(file_types.files || file_types.directories) { + file_types.files = true; + file_types.directories = true; + } + file_types }), extensions: matches.values_of("extension").map(|exts| { diff --git a/src/walk.rs b/src/walk.rs index fa35d68..33caaa9 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -223,6 +223,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc, config: Arc) { .metadata() .map(|m| fshelper::is_executable(&m)) .unwrap_or(false)) + || (file_types.empty_only && !fshelper::is_empty(&entry)) { return ignore::WalkState::Continue; } else if !(entry_type.is_file() diff --git a/tests/tests.rs b/tests/tests.rs index 0a03ed2..cc719b2 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -644,6 +644,33 @@ fn test_type_executable() { ); } +/// Test `--type empty` +#[test] +fn test_type_empty() { + let te = TestEnv::new(&["dir_empty", "dir_nonempty"], &[]); + + create_file_with_size(te.test_root().join("0_bytes.foo"), 0); + create_file_with_size(te.test_root().join("5_bytes.foo"), 5); + + create_file_with_size(te.test_root().join("dir_nonempty").join("2_bytes.foo"), 2); + + te.assert_output( + &["--type", "empty"], + "0_bytes.foo + dir_empty", + ); + + te.assert_output( + &["--type", "empty", "--type", "file", "--type", "directory"], + "0_bytes.foo + dir_empty", + ); + + te.assert_output(&["--type", "empty", "--type", "file"], "0_bytes.foo"); + + te.assert_output(&["--type", "empty", "--type", "directory"], "dir_empty"); +} + /// File extension (--extension) #[test] fn test_extension() {