Просмотр исходного кода

Merge pull request #533 from Xemptuous/feature/add-recursive-dir-size

feat: added recursive directory parser with `--total-size` flag
Christina Sørensen 2 лет назад
Родитель
Сommit
a41a8208cf

+ 1 - 0
Cargo.lock

@@ -370,6 +370,7 @@ dependencies = [
  "natord",
  "num_cpus",
  "number_prefix",
+ "once_cell",
  "percent-encoding",
  "phf",
  "proc-mounts",

+ 1 - 0
Cargo.toml

@@ -81,6 +81,7 @@ log = "0.4"
 natord = "1.0"
 num_cpus = "1.16"
 number_prefix = "0.4"
+once_cell = "1.18.0"
 percent-encoding = "2.3.0"
 phf = { version = "0.11.2", features = ["macros"] }
 scoped_threadpool = "0.1"

+ 1 - 1
README.md

@@ -356,6 +356,7 @@ These options are available when running with `--long` (`-l`):
 - **--git-repos-no-status**: list whether a directory is a Git repository, but not its status (faster)
 - **--no-git**: suppress Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`)
 - **--time-style**: how to format timestamps. valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). [more about format syntax](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
+- **--total-size**: show recursive directory size
 - **--no-permissions**: suppress the permissions field
 - **-o**, **--octal-permissions**: list each file's permission in octal format
 - **--no-filesize**: suppress the filesize field
@@ -425,4 +426,3 @@ The Nix Flake has a few features:
 ## Star History
 
 [![Star History Chart](https://api.star-history.com/svg?repos=eza-community/eza&type=Date)](https://star-history.com/#eza-community/eza&Date)
-

+ 1 - 0
completions/fish/eza.fish

@@ -94,6 +94,7 @@ complete -c eza -l time-style -d "How to format timestamps" -x -a "
     full-iso\t'Display full ISO timestamps, up to the nanosecond'
     relative\t'Display relative timestamps'
 "
+complete -c eza -l total-size -d "Show recursive directory size"
 complete -c eza -l no-permissions -d "Suppress the permissions field"
 complete -c eza -s o -l octal-permissions -d "List each file's permission in octal format"
 complete -c eza -l no-filesize -d "Suppress the filesize field"

+ 1 - 0
completions/nush/eza.nu

@@ -42,6 +42,7 @@ export extern "eza" [
     --accessed(-u)             # Use the accessed timestamp field
     --created(-U)              # Use the created timestamp field
     --time-style               # How to format timestamps
+    --total-size               # Show recursive directory size
     --no-permissions           # Suppress the permissions field
     --octal-permissions(-o)    # List each file's permission in octal format
     --no-filesize              # Suppress the filesize field

+ 1 - 0
completions/zsh/_eza

@@ -49,6 +49,7 @@ __eza() {
         {-S,--blocksize}"[List each file's size of allocated file system blocks.]" \
         {-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \
         --time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso relative)" \
+        --total-size="[Show recursive directory size]" \
         --no-permissions"[Suppress the permissions field]" \
         {-o,--octal-permissions}"[List each file's permission in octal format]" \
         --no-filesize"[Suppress the filesize field]" \

+ 4 - 1
man/eza.1.md

@@ -201,6 +201,9 @@ These options are available when running with `--long` (`-l`):
 
 : Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). for more details about format syntax, please read: https://docs.rs/chrono/latest/chrono/format/strftime/index.html
 
+`--total-size`
+: Show recursive directory size.
+
 `-u`, `--accessed`
 : Use the accessed timestamp field.
 
@@ -230,7 +233,7 @@ These options are available when running with `--long` (`-l`):
 
 `--git`  [if eza was built with git support]
 : List each file’s Git status, if tracked.
-This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted. Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible if a directory contains a file that has a certain status, it will be shown to have that status.  
+This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted. Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible if a directory contains a file that has a certain status, it will be shown to have that status.
 
 `--git-repos` [if eza was built with git support]
 : List each directory’s Git status, if tracked.

+ 22 - 4
src/fs/dir.rs

@@ -51,6 +51,7 @@ impl Dir {
         git: Option<&'ig GitCache>,
         git_ignoring: bool,
         deref_links: bool,
+        total_size: bool,
     ) -> Files<'dir, 'ig> {
         Files {
             inner: self.contents.iter(),
@@ -60,6 +61,7 @@ impl Dir {
             git,
             git_ignoring,
             deref_links,
+            total_size,
         }
     }
 
@@ -75,6 +77,7 @@ impl Dir {
 }
 
 /// Iterator over reading the contents of a directory as `File` objects.
+#[allow(clippy::struct_excessive_bools)]
 pub struct Files<'dir, 'ig> {
     /// The internal iterator over the paths that have been read already.
     inner: SliceIter<'dir, PathBuf>,
@@ -95,6 +98,9 @@ pub struct Files<'dir, 'ig> {
 
     /// Whether symbolic links should be dereferenced when querying information.
     deref_links: bool,
+
+    /// Whether to calculate the directory size recursively
+    total_size: bool,
 }
 
 impl<'dir, 'ig> Files<'dir, 'ig> {
@@ -131,8 +137,14 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
                     }
                 }
 
-                let file = File::from_args(path.clone(), self.dir, filename, self.deref_links)
-                    .map_err(|e| (path.clone(), e));
+                let file = File::from_args(
+                    path.clone(),
+                    self.dir,
+                    filename,
+                    self.deref_links,
+                    self.total_size,
+                )
+                .map_err(|e| (path.clone(), e));
 
                 // Windows has its own concept of hidden files, when dotfiles are
                 // hidden Windows hidden files should also be filtered out
@@ -169,12 +181,18 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
         match self.dots {
             DotsNext::Dot => {
                 self.dots = DotsNext::DotDot;
-                Some(File::new_aa_current(self.dir).map_err(|e| (Path::new(".").to_path_buf(), e)))
+                Some(
+                    File::new_aa_current(self.dir, self.total_size)
+                        .map_err(|e| (Path::new(".").to_path_buf(), e)),
+                )
             }
 
             DotsNext::DotDot => {
                 self.dots = DotsNext::Files;
-                Some(File::new_aa_parent(self.parent(), self.dir).map_err(|e| (self.parent(), e)))
+                Some(
+                    File::new_aa_parent(self.parent(), self.dir, self.total_size)
+                        .map_err(|e| (self.parent(), e)),
+                )
             }
 
             DotsNext::Files => self.next_visible_file(),

+ 163 - 57
src/fs/file.rs

@@ -1,25 +1,41 @@
 //! Files, and methods and fields to access their metadata.
 
+#[cfg(unix)]
+use std::collections::HashMap;
 use std::io;
 #[cfg(unix)]
 use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
 #[cfg(windows)]
 use std::os::windows::fs::MetadataExt;
 use std::path::{Path, PathBuf};
+#[cfg(unix)]
+use std::sync::Mutex;
 use std::sync::OnceLock;
 
 use chrono::prelude::*;
 
 use log::*;
+#[cfg(unix)]
+use once_cell::sync::Lazy;
 
 use crate::fs::dir::Dir;
 use crate::fs::feature::xattr;
 use crate::fs::feature::xattr::{Attribute, FileAttributes};
 use crate::fs::fields as f;
+use crate::fs::recursive_size::RecursiveSize;
 
 use super::mounts::all_mounts;
 use super::mounts::MountedFs;
 
+// Maps (device_id, inode) => (size_in_bytes, size_in_blocks)
+// Mutex::new is const but HashMap::new is not const requiring us to use lazy
+// initialization.
+// TODO: Replace with std::sync::LazyLock when it is stable.
+#[allow(clippy::type_complexity)]
+#[cfg(unix)]
+static DIRECTORY_SIZE_CACHE: Lazy<Mutex<HashMap<(u64, u64), (u64, u64)>>> =
+    Lazy::new(|| Mutex::new(HashMap::new()));
+
 /// A **File** is a wrapper around one of Rust’s `PathBuf` values, along with
 /// associated data about the file.
 ///
@@ -79,6 +95,9 @@ pub struct File<'dir> {
     /// instead.
     pub deref_links: bool,
 
+    /// The recursive directory size when total_size is used.
+    recursive_size: RecursiveSize,
+
     /// The extended attributes of this file.
     extended_attributes: OnceLock<Vec<Attribute>>,
 
@@ -92,6 +111,7 @@ impl<'dir> File<'dir> {
         parent_dir: PD,
         filename: FN,
         deref_links: bool,
+        total_size: bool,
     ) -> io::Result<File<'dir>>
     where
         PD: Into<Option<&'dir Dir>>,
@@ -106,8 +126,13 @@ impl<'dir> File<'dir> {
         let is_all_all = false;
         let extended_attributes = OnceLock::new();
         let absolute_path = OnceLock::new();
+        let recursive_size = if total_size {
+            RecursiveSize::Unknown
+        } else {
+            RecursiveSize::None
+        };
 
-        Ok(File {
+        let mut file = File {
             name,
             ext,
             path,
@@ -115,13 +140,24 @@ impl<'dir> File<'dir> {
             parent_dir,
             is_all_all,
             deref_links,
+            recursive_size,
             extended_attributes,
             absolute_path,
-        })
+        };
+
+        if total_size {
+            file.recursive_size = file.recursive_directory_size();
+        }
+
+        Ok(file)
     }
 
-    pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
-        let path = parent_dir.path.clone();
+    fn new_aa(
+        path: PathBuf,
+        parent_dir: &'dir Dir,
+        name: &'static str,
+        total_size: bool,
+    ) -> io::Result<File<'dir>> {
         let ext = File::ext(&path);
 
         debug!("Statting file {:?}", &path);
@@ -130,41 +166,42 @@ impl<'dir> File<'dir> {
         let parent_dir = Some(parent_dir);
         let extended_attributes = OnceLock::new();
         let absolute_path = OnceLock::new();
+        let recursive_size = if total_size {
+            RecursiveSize::Unknown
+        } else {
+            RecursiveSize::None
+        };
 
-        Ok(File {
+        let mut file = File {
+            name: name.into(),
+            ext,
             path,
-            parent_dir,
             metadata,
-            ext,
-            name: ".".into(),
+            parent_dir,
             is_all_all,
             deref_links: false,
             extended_attributes,
             absolute_path,
-        })
-    }
+            recursive_size,
+        };
 
-    pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
-        let ext = File::ext(&path);
+        if total_size {
+            file.recursive_size = file.recursive_directory_size();
+        }
 
-        debug!("Statting file {:?}", &path);
-        let metadata = std::fs::symlink_metadata(&path)?;
-        let is_all_all = true;
-        let parent_dir = Some(parent_dir);
-        let extended_attributes = OnceLock::new();
-        let absolute_path = OnceLock::new();
+        Ok(file)
+    }
 
-        Ok(File {
-            path,
-            parent_dir,
-            metadata,
-            ext,
-            name: "..".into(),
-            is_all_all,
-            deref_links: false,
-            extended_attributes,
-            absolute_path,
-        })
+    pub fn new_aa_current(parent_dir: &'dir Dir, total_size: bool) -> io::Result<File<'dir>> {
+        File::new_aa(parent_dir.path.clone(), parent_dir, ".", total_size)
+    }
+
+    pub fn new_aa_parent(
+        path: PathBuf,
+        parent_dir: &'dir Dir,
+        total_size: bool,
+    ) -> io::Result<File<'dir>> {
+        File::new_aa(path, parent_dir, "..", total_size)
     }
 
     /// A file’s name is derived from its string. This needs to handle directories
@@ -375,6 +412,7 @@ impl<'dir> File<'dir> {
                     deref_links: self.deref_links,
                     extended_attributes,
                     absolute_path: absolute_path_cell,
+                    recursive_size: RecursiveSize::None,
                 };
                 FileTarget::Ok(Box::new(file))
             }
@@ -437,6 +475,10 @@ impl<'dir> File<'dir> {
                 FileTarget::Ok(f) => f.blocksize(),
                 _ => f::Blocksize::None,
             }
+        } else if self.is_directory() {
+            self.recursive_size.map_or(f::Blocksize::None, |_, blocks| {
+                f::Blocksize::Some(blocks * 512)
+            })
         } else if self.is_file() {
             // Note that metadata.blocks returns the number of blocks
             // for 512 byte blocks according to the POSIX standard
@@ -453,10 +495,10 @@ impl<'dir> File<'dir> {
     #[cfg(unix)]
     pub fn user(&self) -> Option<f::User> {
         if self.is_link() && self.deref_links {
-            match self.link_target_recurse() {
-                FileTarget::Ok(f) => return f.user(),
-                _ => return None,
-            }
+            return match self.link_target_recurse() {
+                FileTarget::Ok(f) => f.user(),
+                _ => None,
+            };
         }
         Some(f::User(self.metadata.uid()))
     }
@@ -465,26 +507,25 @@ impl<'dir> File<'dir> {
     #[cfg(unix)]
     pub fn group(&self) -> Option<f::Group> {
         if self.is_link() && self.deref_links {
-            match self.link_target_recurse() {
-                FileTarget::Ok(f) => return f.group(),
-                _ => return None,
-            }
+            return match self.link_target_recurse() {
+                FileTarget::Ok(f) => f.group(),
+                _ => None,
+            };
         }
         Some(f::Group(self.metadata.gid()))
     }
 
     /// This file’s size, if it’s a regular file.
     ///
-    /// For directories, no size is given. Although they do have a size on
-    /// some filesystems, I’ve never looked at one of those numbers and gained
-    /// any information from it. So it’s going to be hidden instead.
+    /// For directories, the recursive size or no size is given depending on
+    /// flags. Although they do have a size on some filesystems, I’ve never
+    /// looked at one of those numbers and gained any information from it.
     ///
     /// Block and character devices return their device IDs, because they
     /// usually just have a file size of zero.
     ///
     /// Links will return the size of their target (recursively through other
-    /// links) if dereferencing is enabled, otherwise the size of the link
-    /// itself.
+    /// links) if dereferencing is enabled, otherwise None.
     #[cfg(unix)]
     pub fn size(&self) -> f::Size {
         if self.deref_links && self.is_link() {
@@ -492,6 +533,9 @@ impl<'dir> File<'dir> {
                 FileTarget::Ok(f) => f.size(),
                 _ => f::Size::None,
             }
+        } else if self.is_directory() {
+            self.recursive_size
+                .map_or(f::Size::None, |bytes, _| f::Size::Some(bytes))
         } else if self.is_char_device() || self.is_block_device() {
             let device_id = self.metadata.rdev();
 
@@ -509,7 +553,7 @@ impl<'dir> File<'dir> {
         } else if self.is_file() {
             f::Size::Some(self.metadata.len())
         } else {
-            // directory and symlink
+            // symlink
             f::Size::None
         }
     }
@@ -527,6 +571,68 @@ impl<'dir> File<'dir> {
         }
     }
 
+    /// Calculate the total directory size recursively.  If not a directory `None`
+    /// will be returned.  The directory size is cached for recursive directory
+    /// listing.
+    #[cfg(unix)]
+    fn recursive_directory_size(&self) -> RecursiveSize {
+        if self.is_directory() {
+            let key = (self.metadata.dev(), self.metadata.ino());
+            if let Some(size) = DIRECTORY_SIZE_CACHE.lock().unwrap().get(&key) {
+                return RecursiveSize::Some(size.0, size.1);
+            }
+            Dir::read_dir(self.path.clone()).map_or(RecursiveSize::Unknown, |dir| {
+                let mut size = 0;
+                let mut blocks = 0;
+                for file in dir
+                    .files(super::DotFilter::Dotfiles, None, false, false, true)
+                    .flatten()
+                {
+                    match file.recursive_directory_size() {
+                        RecursiveSize::Some(bytes, blks) => {
+                            size += bytes;
+                            blocks += blks;
+                        }
+                        RecursiveSize::Unknown => {}
+                        RecursiveSize::None => {
+                            size += file.metadata.size();
+                            blocks += file.metadata.blocks();
+                        }
+                    }
+                }
+                DIRECTORY_SIZE_CACHE
+                    .lock()
+                    .unwrap()
+                    .insert(key, (size, blocks));
+                RecursiveSize::Some(size, blocks)
+            })
+        } else {
+            RecursiveSize::None
+        }
+    }
+
+    /// Windows version always returns None.  The metadata for
+    /// `volume_serial_number` and `file_index` are marked unstable so we can
+    /// not cache the sizes.  Without caching we could end up walking the
+    /// directory structure several times.
+    #[cfg(windows)]
+    fn recursive_directory_size(&self) -> RecursiveSize {
+        RecursiveSize::None
+    }
+
+    /// Returns the same value as `self.metadata.len()` or the recursive size
+    /// of a directory when `total_size` is used.
+    #[inline]
+    pub fn length(&self) -> u64 {
+        self.recursive_size.unwrap_bytes_or(self.metadata.len())
+    }
+
+    /// Is the file is using recursive size calculation
+    #[inline]
+    pub fn is_recursive_size(&self) -> bool {
+        !self.recursive_size.is_none()
+    }
+
     /// Determines if the directory is empty or not.
     ///
     /// For Unix platforms, this function first checks the link count to quickly
@@ -581,7 +687,7 @@ impl<'dir> File<'dir> {
         match Dir::read_dir(self.path.clone()) {
             // . & .. are skipped, if the returned iterator has .next(), it's not empty
             Ok(has_files) => has_files
-                .files(super::DotFilter::Dotfiles, None, false, false)
+                .files(super::DotFilter::Dotfiles, None, false, false, false)
                 .next()
                 .is_none(),
             Err(_) => false,
@@ -691,10 +797,10 @@ impl<'dir> File<'dir> {
             // If the chain of links is broken, we instead fall through and
             // return the permissions of the original link, as would have been
             // done if we were not dereferencing.
-            match self.link_target_recurse() {
-                FileTarget::Ok(f) => return f.permissions(),
-                _ => return None,
-            }
+            return match self.link_target_recurse() {
+                FileTarget::Ok(f) => f.permissions(),
+                _ => None,
+            };
         }
         let bits = self.metadata.mode();
         let has_bit = |bit| bits & bit == bit;
@@ -814,17 +920,17 @@ mod ext_test {
 
     #[test]
     fn extension() {
-        assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat")))
+        assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat")));
     }
 
     #[test]
     fn dotfile() {
-        assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")))
+        assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")));
     }
 
     #[test]
     fn no_extension() {
-        assert_eq!(None, File::ext(Path::new("jarlsberg")))
+        assert_eq!(None, File::ext(Path::new("jarlsberg")));
     }
 }
 
@@ -835,32 +941,32 @@ mod filename_test {
 
     #[test]
     fn file() {
-        assert_eq!("fester.dat", File::filename(Path::new("fester.dat")))
+        assert_eq!("fester.dat", File::filename(Path::new("fester.dat")));
     }
 
     #[test]
     fn no_path() {
-        assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha")))
+        assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha")));
     }
 
     #[test]
     fn here() {
-        assert_eq!(".", File::filename(Path::new(".")))
+        assert_eq!(".", File::filename(Path::new(".")));
     }
 
     #[test]
     fn there() {
-        assert_eq!("..", File::filename(Path::new("..")))
+        assert_eq!("..", File::filename(Path::new("..")));
     }
 
     #[test]
     fn everywhere() {
-        assert_eq!("..", File::filename(Path::new("./..")))
+        assert_eq!("..", File::filename(Path::new("./..")));
     }
 
     #[test]
     #[cfg(unix)]
     fn topmost() {
-        assert_eq!("/", File::filename(Path::new("/")))
+        assert_eq!("/", File::filename(Path::new("/")));
     }
 }

+ 3 - 2
src/fs/filter.rs

@@ -240,7 +240,8 @@ impl SortField {
             Self::Name(ABCabc)  => natord::compare(&a.name, &b.name),
             Self::Name(AaBbCc)  => natord::compare_ignore_case(&a.name, &b.name),
 
-            Self::Size          => a.metadata.len().cmp(&b.metadata.len()),
+            Self::Size          => a.length().cmp(&b.length()),
+
             #[cfg(unix)]
             Self::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
             Self::ModifiedDate  => a.modified_time().cmp(&b.modified_time()),
@@ -271,7 +272,7 @@ impl SortField {
             Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case(
                 Self::strip_dot(&a.name),
                 Self::strip_dot(&b.name)
-            )
+            ),
         };
     }
 

+ 1 - 0
src/fs/mod.rs

@@ -9,3 +9,4 @@ pub mod feature;
 pub mod fields;
 pub mod filter;
 pub mod mounts;
+pub mod recursive_size;

+ 80 - 0
src/fs/recursive_size.rs

@@ -0,0 +1,80 @@
+/// Used to represent a the size of a recursive directory traversal.  `None`
+/// should be used when the file does not represent a directory or the recursive
+/// size should not be calculated.
+#[derive(Copy, Clone, Debug)]
+pub enum RecursiveSize {
+    /// Size should not be computed
+    None,
+    /// Size should be computed but has not been computed yet
+    Unknown,
+    /// Size has been computed.  First field is size in bytes and second field
+    /// is size in blocks
+    #[cfg_attr(target_family = "windows", allow(dead_code))]
+    Some(u64, u64),
+}
+
+impl RecursiveSize {
+    /// Returns `true` if `None`
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use eza::fs::recursive_size::RecursiveSize;
+    ///
+    /// let x = RecursiveSize::None;
+    /// assert_eq!(x.is_none(), true);
+    ///
+    /// let x = RecursiveSize::Unknown;
+    /// assert_eq!(x.is_none(), false);
+    ///
+    /// let x = RecursiveSize::Some(0, 0);
+    /// assert_eq!(x.is_none(), false);
+    /// ```
+    #[inline]
+    pub const fn is_none(&self) -> bool {
+        matches!(*self, Self::None)
+    }
+
+    /// Returns the contained [`Some`] value or a provided default.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use eza::fs::recursive_size::RecursiveSize;
+    ///
+    /// assert_eq!(RecursiveSize::None.unwrap_bytes_or(1), 1);
+    /// assert_eq!(RecursiveSize::Unknown.unwrap_bytes_or(1), 1);
+    /// assert_eq!(RecursiveSize::Some(2, 3).unwrap_bytes_or(1), 2);
+    /// ```
+    #[inline]
+    pub const fn unwrap_bytes_or(self, default: u64) -> u64 {
+        match self {
+            Self::Some(bytes, _blocks) => bytes,
+            _ => default,
+        }
+    }
+
+    /// Returns the provided default result (if None or Unknown),
+    /// or applies a function to the contained value (if Some).
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use eza::fs::recursive_size::RecursiveSize;
+    ///
+    /// assert_eq!(RecursiveSize::None.map_or(None, |s, _| Some(s * 2)), None);
+    /// assert_eq!(RecursiveSize::Unknown.map_or(None, |s, _| Some(s * 2)), None);
+    /// assert_eq!(RecursiveSize::Some(2, 3).map_or(None, |s, _| Some(s * 2)), Some(4));
+    /// ```
+    #[inline]
+    #[cfg_attr(target_family = "windows", allow(dead_code))]
+    pub fn map_or<U, F>(self, default: U, f: F) -> U
+    where
+        F: FnOnce(u64, u64) -> U,
+    {
+        match self {
+            RecursiveSize::Some(bytes, blocks) => f(bytes, blocks),
+            _ => default,
+        }
+    }
+}

+ 3 - 1
src/main.rs

@@ -187,6 +187,7 @@ impl<'args> Exa<'args> {
                 None,
                 None,
                 self.options.view.deref_links,
+                self.options.view.total_size,
             ) {
                 Err(e) => {
                     exit_status = 2;
@@ -263,6 +264,7 @@ impl<'args> Exa<'args> {
                 self.git.as_ref(),
                 git_ignore,
                 self.options.view.deref_links,
+                self.options.view.total_size,
             ) {
                 match file {
                     Ok(file) => children.push(file),
@@ -397,8 +399,8 @@ impl<'args> Exa<'args> {
                 let filter = &self.options.filter;
                 let recurse = self.options.dir_action.recurse_options();
                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
-
                 let git = self.git.as_ref();
+
                 let r = details::Render {
                     dir,
                     files,

+ 21 - 20
src/options/flags.rs

@@ -14,7 +14,7 @@ pub static TREE:        Arg = Arg { short: Some(b'T'), long: "tree",        take
 pub static CLASSIFY:    Arg = Arg { short: Some(b'F'), long: "classify",    takes_value: TakesValue::Forbidden };
 pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden };
 pub static WIDTH:       Arg = Arg { short: Some(b'w'), long: "width",       takes_value: TakesValue::Necessary(None) };
-pub static NO_QUOTES:Arg = Arg { short: None,          long: "no-quotes",takes_value: TakesValue::Forbidden };
+pub static NO_QUOTES:   Arg = Arg { short: None,       long: "no-quotes",   takes_value: TakesValue::Forbidden };
 
 pub static COLOR:  Arg = Arg { short: None, long: "color",  takes_value: TakesValue::Optional(Some(WHEN)) };
 pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Optional(Some(WHEN)) };
@@ -34,29 +34,30 @@ pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", take
 pub static GIT_IGNORE:  Arg = Arg { short: None, long: "git-ignore",           takes_value: TakesValue::Forbidden };
 pub static DIRS_FIRST:  Arg = Arg { short: None, long: "group-directories-first",  takes_value: TakesValue::Forbidden };
 pub static ONLY_DIRS:   Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden };
-pub static ONLY_FILES:   Arg = Arg { short: Some(b'f'), long: "only-files", takes_value: TakesValue::Forbidden };
+pub static ONLY_FILES:  Arg = Arg { short: Some(b'f'), long: "only-files", takes_value: TakesValue::Forbidden };
 const SORTS: Values = &[ "name", "Name", "size", "extension",
                          "Extension", "modified", "changed", "accessed",
                          "created", "inode", "type", "none" ];
 
 // display options
-pub static BINARY:     Arg = Arg { short: Some(b'b'), long: "binary",     takes_value: TakesValue::Forbidden };
-pub static BYTES:      Arg = Arg { short: Some(b'B'), long: "bytes",      takes_value: TakesValue::Forbidden };
-pub static GROUP:      Arg = Arg { short: Some(b'g'), long: "group",      takes_value: TakesValue::Forbidden };
-pub static NUMERIC:    Arg = Arg { short: Some(b'n'), long: "numeric",    takes_value: TakesValue::Forbidden };
-pub static HEADER:     Arg = Arg { short: Some(b'h'), long: "header",     takes_value: TakesValue::Forbidden };
-pub static ICONS:      Arg = Arg { short: None,       long: "icons",      takes_value: TakesValue::Optional(Some(WHEN))};
-pub static INODE:      Arg = Arg { short: Some(b'i'), long: "inode",      takes_value: TakesValue::Forbidden };
-pub static LINKS:      Arg = Arg { short: Some(b'H'), long: "links",      takes_value: TakesValue::Forbidden };
-pub static MODIFIED:   Arg = Arg { short: Some(b'm'), long: "modified",   takes_value: TakesValue::Forbidden };
-pub static CHANGED:    Arg = Arg { short: None,       long: "changed",    takes_value: TakesValue::Forbidden };
-pub static BLOCKSIZE:  Arg = Arg { short: Some(b'S'), long: "blocksize",  takes_value: TakesValue::Forbidden };
-pub static TIME:       Arg = Arg { short: Some(b't'), long: "time",       takes_value: TakesValue::Necessary(Some(TIMES)) };
-pub static ACCESSED:   Arg = Arg { short: Some(b'u'), long: "accessed",   takes_value: TakesValue::Forbidden };
-pub static CREATED:    Arg = Arg { short: Some(b'U'), long: "created",    takes_value: TakesValue::Forbidden };
-pub static TIME_STYLE: Arg = Arg { short: None,       long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) };
-pub static HYPERLINK:  Arg = Arg { short: None,       long: "hyperlink",  takes_value: TakesValue::Forbidden };
-pub static MOUNTS:     Arg = Arg { short: Some(b'M'), long: "mounts",     takes_value: TakesValue::Forbidden };
+pub static BINARY:      Arg = Arg { short: Some(b'b'), long: "binary",      takes_value: TakesValue::Forbidden };
+pub static BYTES:       Arg = Arg { short: Some(b'B'), long: "bytes",       takes_value: TakesValue::Forbidden };
+pub static GROUP:       Arg = Arg { short: Some(b'g'), long: "group",       takes_value: TakesValue::Forbidden };
+pub static NUMERIC:     Arg = Arg { short: Some(b'n'), long: "numeric",     takes_value: TakesValue::Forbidden };
+pub static HEADER:      Arg = Arg { short: Some(b'h'), long: "header",      takes_value: TakesValue::Forbidden };
+pub static ICONS:       Arg = Arg { short: None,       long: "icons",       takes_value: TakesValue::Optional(Some(WHEN))};
+pub static INODE:       Arg = Arg { short: Some(b'i'), long: "inode",       takes_value: TakesValue::Forbidden };
+pub static LINKS:       Arg = Arg { short: Some(b'H'), long: "links",       takes_value: TakesValue::Forbidden };
+pub static MODIFIED:    Arg = Arg { short: Some(b'm'), long: "modified",    takes_value: TakesValue::Forbidden };
+pub static CHANGED:     Arg = Arg { short: None,       long: "changed",     takes_value: TakesValue::Forbidden };
+pub static BLOCKSIZE:   Arg = Arg { short: Some(b'S'), long: "blocksize",   takes_value: TakesValue::Forbidden };
+pub static TOTAL_SIZE:  Arg = Arg { short: None,       long: "total-size",  takes_value: TakesValue::Forbidden };
+pub static TIME:        Arg = Arg { short: Some(b't'), long: "time",        takes_value: TakesValue::Necessary(Some(TIMES)) };
+pub static ACCESSED:    Arg = Arg { short: Some(b'u'), long: "accessed",    takes_value: TakesValue::Forbidden };
+pub static CREATED:     Arg = Arg { short: Some(b'U'), long: "created",     takes_value: TakesValue::Forbidden };
+pub static TIME_STYLE:  Arg = Arg { short: None,       long: "time-style",  takes_value: TakesValue::Necessary(Some(TIME_STYLES)) };
+pub static HYPERLINK:   Arg = Arg { short: None,       long: "hyperlink",   takes_value: TakesValue::Forbidden };
+pub static MOUNTS:      Arg = Arg { short: Some(b'M'), long: "mounts",      takes_value: TakesValue::Forbidden };
 pub static SMART_GROUP: Arg = Arg { short: None,       long: "smart-group", takes_value: TakesValue::Forbidden };
 const TIMES: Values = &["modified", "changed", "accessed", "created"];
 const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"];
@@ -86,7 +87,7 @@ pub static ALL_ARGS: Args = Args(&[
     &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES,
 
     &BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
-    &BLOCKSIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS,
+    &BLOCKSIZE, &TOTAL_SIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS,
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP,
 
     &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT,

+ 1 - 0
src/options/help.rs

@@ -65,6 +65,7 @@ LONG VIEW OPTIONS
   -U, --created            use the created timestamp field
   --changed                use the changed timestamp field
   --time-style             how to format timestamps (default, iso, long-iso, full-iso, relative, or a custom style with '+' as prefix. Ex: '+%Y/%m/%d')
+  --total-size             show the size of a directory as the size of all files and directories inside
   --no-permissions         suppress the permissions field
   -o, --octal-permissions  list each file's permission in octal format
   --no-filesize            suppress the filesize field

+ 2 - 0
src/options/view.rs

@@ -13,6 +13,7 @@ impl View {
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         let mode = Mode::deduce(matches, vars)?;
         let deref_links = matches.has(&flags::DEREF_LINKS)?;
+        let total_size = matches.has(&flags::TOTAL_SIZE)?;
         let width = TerminalWidth::deduce(matches, vars)?;
         let file_style = FileStyle::deduce(matches, vars, width.actual_terminal_width().is_some())?;
         Ok(Self {
@@ -20,6 +21,7 @@ impl View {
             width,
             file_style,
             deref_links,
+            total_size,
         })
     }
 }

+ 1 - 0
src/output/details.rs

@@ -347,6 +347,7 @@ impl<'a> Render<'a> {
                     self.git,
                     self.git_ignoring,
                     egg.file.deref_links,
+                    egg.file.is_recursive_size(),
                 ) {
                     match file_to_add {
                         Ok(f) => {

+ 1 - 0
src/output/mod.rs

@@ -22,6 +22,7 @@ pub struct View {
     pub width: TerminalWidth,
     pub file_style: file_name::Options,
     pub deref_links: bool,
+    pub total_size: bool,
 }
 
 /// The **mode** is the “type” of output.