Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ eza’s options are almost, but not quite, entirely unlike `ls`’s. Quick overv
- **--hyperlink**: display entries as hyperlinks
- **--absolute=(mode)**: display entries with their absolute path (on, follow, off)
- **-w**, **--width=(columns)**: set screen width in columns
- **--summary**: show summary of files, directories, and symlinks

</details>

Expand Down
2 changes: 1 addition & 1 deletion completions/bash/eza
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ _eza() {
prev=${COMP_WORDS[COMP_CWORD-1]}

case "$prev" in
--help|-v|--version|--smart-group)
--help|-v|--version|--smart-group|--summary)
return
;;

Expand Down
1 change: 1 addition & 0 deletions completions/fish/eza.fish
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ complete -c eza -l absolute -d "Display entries with their absolute path" -x -a
off\t'Do not show the absolute path'
"
complete -c eza -l smart-group -d "Only show group if it has a different name from owner"
complete -c eza -l summary -d "Show summary of files, directories, and symlinks"

# Filtering and sorting options
complete -c eza -l group-directories-first -d "Sort directories before other files"
Expand Down
1 change: 1 addition & 0 deletions completions/nush/eza.nu
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export extern "eza" [
--hyperlink # Display entries as hyperlinks
--absolute # Display entries with their absolute path
--follow-symlinks # Drill down into symbolic links that point to directories
--summary # Show summary of files, directories, and symlinks
--group-directories-first # Sort directories before other files
--group-directories-last # Sort directories after other files
--git-ignore # Ignore files mentioned in '.gitignore'
Expand Down
3 changes: 2 additions & 1 deletion completions/zsh/_eza
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ __eza() {
{-M,--mounts}"[Show mount details (long mode only)]" \
'*:filename:_files' \
--smart-group"[Only show group if it has a different name from owner]" \
--stdin"[When piping to eza. Read file names from stdin]"
--stdin"[When piping to eza. Read file names from stdin]" \
--summary"[Show summary of files, directories, and symlinks]"
}

__eza
5 changes: 5 additions & 0 deletions man/eza.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ When used without a value, defaults to ‘`automatic`’.
`-w`, `--width=COLS`
: Set screen width in columns.

`--summary`
: Display a summary showing the count of directories, files, and symlinks at the end of the output.

When used with `--recurse` or `--tree`, the summary includes counts of all items displayed recursively. If the `--icons` option is enabled, the summary will display with corresponding icons next to each category.

FILTERING AND SORTING OPTIONS
=============================

Expand Down
128 changes: 126 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ fn main() {
console_width,
git,
git_repos,
summary: Summary::default(),
};

info!("matching on exa.run");
Expand Down Expand Up @@ -132,6 +133,30 @@ fn main() {
}
}

/// Summary statistics for file counts.
#[derive(Default, Debug)]
pub struct Summary {
pub directories: usize,
pub files: usize,
pub symlinks: usize,
}

impl Summary {
fn total(&self) -> usize {
self.directories + self.files + self.symlinks
}

fn add_file(&mut self, file: &File<'_>) {
if file.is_link() {
self.symlinks += 1;
} else if file.is_directory() {
self.directories += 1;
} else {
self.files += 1;
}
}
}

/// The main program wrapper.
pub struct Exa<'args> {
/// List of command-line options, having been successfully parsed.
Expand All @@ -141,7 +166,7 @@ pub struct Exa<'args> {
pub writer: io::Stdout,

/// List of the free command-line arguments that should correspond to file
/// names (anything that isnt an option).
/// names (anything that isn't an option).
pub input_paths: Vec<&'args OsStr>,

/// The theme that has been configured from the command-line options and
Expand All @@ -159,6 +184,9 @@ pub struct Exa<'args> {
pub git: Option<GitCache>,

pub git_repos: bool,

/// Summary statistics tracking.
pub summary: Summary,
}

/// The “real” environment variables type.
Expand Down Expand Up @@ -284,7 +312,14 @@ impl Exa<'_> {
self.options.filter.filter_argument_files(&mut files);
self.print_files(None, files)?;

self.print_dirs(dirs, no_files, is_only_dir, exit_status)
let exit_code = self.print_dirs(dirs, no_files, is_only_dir, exit_status)?;

// Print summary if enabled
if self.options.view.summary {
self.print_summary()?;
}

Ok(exit_code)
}

fn print_dirs(
Expand Down Expand Up @@ -404,10 +439,58 @@ impl Exa<'_> {
}

/// Prints the list of files using whichever view is selected.
/// Recursively count files in a directory for tree/recursive modes
fn count_files_recursive(&mut self, file: &File<'_>) {
self.summary.add_file(file);

if file.is_directory() {
if let Ok(dir) = file.read_dir() {
let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
// Collect files first to avoid borrow checker issues
let children: Vec<_> = dir
.files(
self.options.filter.dot_filter,
self.git.as_ref(),
git_ignore,
self.options.view.deref_links,
self.options.view.total_size,
)
.collect();

for child in children {
self.count_files_recursive(&child);
}
}
}
}

fn print_files(&mut self, dir: Option<&Dir>, mut files: Vec<File<'_>>) -> io::Result<()> {
if files.is_empty() {
return Ok(());
}

// Count files for summary if enabled
if self.options.view.summary {
let is_tree_mode = self
.options
.dir_action
.recurse_options()
.map(|r| r.tree)
.unwrap_or(false);

if is_tree_mode {
// In tree mode, we need to recursively count all files
for file in &files {
self.count_files_recursive(file);
}
} else {
// In other modes, just count the files being printed
for file in &files {
self.summary.add_file(file);
}
}
}

let recursing = self.options.dir_action.recurse_options().is_some();
let only_files = self.options.filter.flags.contains(&OnlyFiles);
if recursing && only_files {
Expand Down Expand Up @@ -532,6 +615,47 @@ impl Exa<'_> {
}
}
}

/// Print the summary statistics.
fn print_summary(&mut self) -> io::Result<()> {
use crate::output::file_name::ShowIcons;

writeln!(&mut self.writer)?;

let show_icons = match self.options.view.file_style.show_icons {
ShowIcons::Always(_) => true,
ShowIcons::Automatic(_) => io::stdout().is_terminal(),
ShowIcons::Never => false,
};

if show_icons {
// With icons
writeln!(
&mut self.writer,
"\u{e5ff} Directories: {}",
self.summary.directories
)?;
writeln!(&mut self.writer, "\u{f15b} Files: {}", self.summary.files)?;
writeln!(
&mut self.writer,
"\u{f0338} Symlinks: {}",
self.summary.symlinks
)?;
} else {
// Without icons
writeln!(
&mut self.writer,
"Directories: {}",
self.summary.directories
)?;
writeln!(&mut self.writer, "Files: {}", self.summary.files)?;
writeln!(&mut self.writer, "Symlinks: {}", self.summary.symlinks)?;
}

writeln!(&mut self.writer, "Total: {}", self.summary.total())?;

Ok(())
}
}

mod exits {
Expand Down
1 change: 1 addition & 0 deletions src/options/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub fn get_command() -> clap::Command {
.default_missing_value("auto"))
.arg(arg!(--hyperlink "display entries as hyperlinks"))
.arg(arg!(--"no-quotes" "don't quote file names with spaces"))
.arg(arg!(--summary "show summary of files, directories, and symlinks"))

.next_help_heading("FILTERING OPTIONS")
.arg(arg!(-a --all... "show hidden files. Use this twice to also show the '.' and '..' directories"))
Expand Down
2 changes: 2 additions & 0 deletions src/options/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ impl View {
let deref_links = matches.get_flag("dereference");
let follow_links = matches.get_flag("follow-symlinks");
let total_size = matches.get_flag("total-size");
let summary = matches.get_flag("summary");
let file_style = FileStyle::deduce(matches, vars, is_tty)?;
Ok(Self {
mode,
Expand All @@ -43,6 +44,7 @@ impl View {
deref_links,
follow_links,
total_size,
summary,
})
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ mod tree;

/// The **view** contains all information about how to format output.
#[derive(Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct View {
pub mode: Mode,
pub width: TerminalWidth,
pub file_style: file_name::Options,
pub deref_links: bool,
pub follow_links: bool,
pub total_size: bool,
pub summary: bool,
}

/// The **mode** is the “type” of output.
Expand Down
Empty file added tests/cmd/summary_all.stderr
Empty file.
27 changes: 27 additions & 0 deletions tests/cmd/summary_all.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
a
b
c
d
dir-symlink -> vagrant/debug
e
exa
f
g
h
i
image.jpg.img.c.rs.log.png
index.svg
j
k
l
m
n
o
p
q
vagrant

Directories: 2
Files: 19
Symlinks: 1
Total: 22
2 changes: 2 additions & 0 deletions tests/cmd/summary_all.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin.name = "eza"
args = "tests/itest --summary --icons=never"
Empty file.
27 changes: 27 additions & 0 deletions tests/cmd/summary_icons_all.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
󰡯 a
󰡯 b
󰡯 c
󰡯 d
 dir-symlink -> vagrant/debug
󰡯 e
 exa
󰡯 f
󰡯 g
󰡯 h
󰡯 i
 image.jpg.img.c.rs.log.png
󰕙 index.svg
󰡯 j
󰡯 k
󰡯 l
󰡯 m
󰡯 n
󰡯 o
󰡯 p
󰡯 q
 vagrant

 Directories: 2
 Files: 19
󰌸 Symlinks: 1
Total: 22
2 changes: 2 additions & 0 deletions tests/cmd/summary_icons_all.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin.name = "eza"
args = "tests/itest --summary --icons=always"
Empty file.
42 changes: 42 additions & 0 deletions tests/cmd/summary_tree_unix.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
tests/itest
├── a
├── b
├── c
├── d
├── dir-symlink -> vagrant/debug
├── e
├── exa
│ ├── file.c -> djihisudjuhfius
│ └── sssssssssssssssssssssssssggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggsssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
│ └── Makefile
├── f
├── g
├── h
├── i
├── image.jpg.img.c.rs.log.png
├── index.svg
├── j
├── k
├── l
├── m
├── n
├── o
├── p
├── q
└── vagrant
├── debug
│ ├── a
│ ├── symlink -> a
│ └── symlink-broken -> ./b
├── dev
│ └── main.bf
└── log
├── file.png
└── run
├── run.log.text
└── sps.log.text

Directories: 8
Files: 25
Symlinks: 4
Total: 37
2 changes: 2 additions & 0 deletions tests/cmd/summary_tree_unix.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin.name = "eza"
args = "tests/itest --tree --summary --icons=never"
Empty file.
15 changes: 15 additions & 0 deletions tests/gen/summary_long_nix.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
drwxr-xr-x - nixbld 1 Jan 1970 git
drwxr-xr-x - nixbld 1 Jan 1970 grid
drwxr-xr-x - nixbld 1 Jan 1970 group
drwxr-xr-x - nixbld 1 Jan 1970 icons
drwxr-xr-x - nixbld 1 Jan 1970 perms
drwxrwxrwx - root 1 Jan 1970 root
drwxr-xr-x - nixbld 1 Jan 1970 size
drwxr-xr-x - nixbld 1 Jan 1970 specials
drwxr-xr-x - nixbld 1 Jan 1970 symlinks
drwxr-xr-x - nixbld 1 Jan 1970 time

Directories: 10
Files: 0
Symlinks: 0
Total: 10
Loading
Loading