Repository: eza-community/eza (language: rust)

Issue:
bug: --only-files breaks --recurse
# System context

- eza v0.19.0 \[+git\]
- Invoking `eza --recurse --only-files`
- zsh 5.9 (x86_64-apple-darwin23.0) running in iTerm2 3.5.2
- macOS 14.6.1 (Sonoma) running on a 2021 MacBook Pro (M1)

# Observed behaviour

eza enumerates only files in the target directory.

# Expected behaviour

eza recursively enumerates files contained in the target directory and its subdirectories.

# Notes

While I can appreciate that `--only-files` says "don't look at directories", this behaviour leaves eza with no way to recursively enumerate files in a directory tree without also enumerating the directories themselves. Specifying `--recurse` should modify the behaviour of `--only-files` to match the UX expectation here.


Discussion / hints:

Fix patch:
diff --git a/src/fs/filter.rs b/src/fs/filter.rs
index 2fe8a4e19..52ba5e88d 100644
--- a/src/fs/filter.rs
+++ b/src/fs/filter.rs
@@ -85,38 +85,28 @@ pub struct FileFilter {
 impl FileFilter {
     /// Remove every file in the given vector that does *not* pass the
     /// filter predicate for files found inside a directory.
-    pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
+    #[rustfmt::skip]
+    pub fn filter_child_files(&self, is_recurse: bool, files: &mut Vec<File<'_>>) {
         use FileFilterFlags::{NoSymlinks, OnlyDirs, OnlyFiles, ShowSymlinks};
 
         files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
-
-        match (
-            self.flags.contains(&OnlyDirs),
-            self.flags.contains(&OnlyFiles),
-            self.flags.contains(&NoSymlinks),
-            self.flags.contains(&ShowSymlinks),
-        ) {
-            (true, false, false, false) => {
-                // On pass '--only-dirs' flag only
-                files.retain(File::is_directory);
-            }
-            (true, false, true, false) => {
-                files.retain(File::is_directory);
-            }
-            (true, false, false, true) => {
-                files.retain(|f| f.is_directory() || f.points_to_directory());
-            }
-            (false, true, false, false) => {
-                files.retain(File::is_file);
+        files.retain(|f| {
+            match (
+                self.flags.contains(&OnlyDirs),
+                self.flags.contains(&OnlyFiles),
+                self.flags.contains(&NoSymlinks),
+                self.flags.contains(&ShowSymlinks),
+            ) {
+                (true, false, false, false) => f.is_directory(),
+                (true, false, true, false) => f.is_directory(),
+                (true, false, false, true) => f.is_directory() || f.points_to_directory(),
+                (false, true, false, false) => if is_recurse { true } else {f.is_file() },
+                (false, true, false, true) => if is_recurse { true } else { f.is_file() || f.is_link() && !f.points_to_directory()
+                },
+                (false, false, true, false) => !f.is_link(),
+                _ => true,
             }
-            (false, true, false, true) => {
-                files.retain(|f| f.is_file() || f.is_link() && !f.points_to_directory());
-            }
-            (false, false, true, false) => {
-                files.retain(|f| !f.is_link());
-            }
-            _ => {}
-        }
+        });
     }
 
     /// Remove every file in the given vector that does *not* pass the
diff --git a/src/main.rs b/src/main.rs
index 162e950cc..0ebb24092 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,7 +30,7 @@ use std::process::exit;
 use nu_ansi_term::{AnsiStrings as ANSIStrings, Style};
 
 use crate::fs::feature::git::GitCache;
-use crate::fs::filter::GitIgnore;
+use crate::fs::filter::{FileFilterFlags::OnlyFiles, GitIgnore};
 use crate::fs::{Dir, File};
 use crate::options::stdin::FilesInput;
 use crate::options::{vars, Options, OptionsResult, Vars};
@@ -343,8 +343,10 @@ impl<'args> Exa<'args> {
                     Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
                 }
             }
-
-            self.options.filter.filter_child_files(&mut children);
+            let recursing = self.options.dir_action.recurse_options().is_some();
+            self.options
+                .filter
+                .filter_child_files(recursing, &mut children);
             self.options.filter.sort_files(&mut children);
 
             if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
@@ -384,11 +386,18 @@ impl<'args> Exa<'args> {
     }
 
     /// Prints the list of files using whichever view is selected.
-    fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
+    fn print_files(&mut self, dir: Option<&Dir>, mut files: Vec<File<'_>>) -> io::Result<()> {
         if files.is_empty() {
             return Ok(());
         }
-
+        let recursing = self.options.dir_action.recurse_options().is_some();
+        let only_files = self.options.filter.flags.contains(&OnlyFiles);
+        if recursing && only_files {
+            files = files
+                .into_iter()
+                .filter(|f| !f.is_directory())
+                .collect::<Vec<_>>();
+        }
         let theme = &self.theme;
         let View {
             ref mode,
diff --git a/src/output/details.rs b/src/output/details.rs
index 1a3275a8c..5a72b00a7 100644
--- a/src/output/details.rs
+++ b/src/output/details.rs
@@ -356,7 +356,8 @@ impl<'a> Render<'a> {
                     }
                 }
 
-                self.filter.filter_child_files(&mut files);
+                self.filter
+                    .filter_child_files(self.recurse.is_some(), &mut files);
 
                 if !files.is_empty() {
                     for xattr in egg.xattrs {


Test patch: