Repository: sharkdp/fd (language: rust)

Issue:
Option for filtering empty files/directories
This is something that find has that would be very nice to have.


Discussion / hints:
Thank you for your feedback!

In my point of view, searching for empty files/directories is a rather niche use case and I would only consider adding it, if there is a strong support for this (see the part about "80% of the use cases" in the README).

Also, I would like us to think about alternatives to a new `--empty` command line option. One option could be to extend the `--type` option with a new "empty" type. Another option could be to use `--size 0` (if we decide to move forward with #276).

Fix patch:
diff --git a/doc/fd.1 b/doc/fd.1
index 450bdad5d..3099a9136 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 e79e2a189..7496b5c58 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 37677e6bf..33f6fd7f6 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<PathBuf> {
     if path.is_absolute() {
         Ok(path.to_path_buf())
@@ -36,7 +38,7 @@ pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
     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 5bffb7a55..263571d5a 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 9f2c4a170..0de41f570 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 fa35d6819..33caaa928 100644
--- a/src/walk.rs
+++ b/src/walk.rs
@@ -223,6 +223,7 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) {
                             .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()


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index 0a03ed2e4..cc719b2c5 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() {