diff options
author | Benjamin Nguyen <benjamin.van.nguyen@gmail.com> | 2023-11-30 15:06:26 -0800 |
---|---|---|
committer | Benjamin Nguyen <benjamin.van.nguyen@gmail.com> | 2023-11-30 15:06:26 -0800 |
commit | 6a9e95b6df1b27bcc1f0a6874d9e5567ef3905e2 (patch) | |
tree | e102d0025fc46318fa155d9aaf34085083c680ac | |
parent | 867859b3ae2e3b6a7f68563477025bfd7bf2f1cf (diff) |
filter by file type
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | src/file/mod.rs | 51 | ||||
-rw-r--r-- | src/file/tree/filter.rs | 61 | ||||
-rw-r--r-- | src/file/tree/mod.rs | 5 | ||||
-rw-r--r-- | src/file/tree/traverse.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 4 | ||||
-rw-r--r-- | src/user/args.rs | 36 | ||||
-rw-r--r-- | src/user/mod.rs | 36 |
8 files changed, 178 insertions, 18 deletions
@@ -1,2 +1,3 @@ /target .DS_Store +foobar diff --git a/src/file/mod.rs b/src/file/mod.rs index db1ba20..3f34397 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -14,6 +14,9 @@ use std::{ path::{Path, PathBuf}, }; +#[cfg(unix)] +use crate::file::unix::permissions::{file_type::FileType, SymbolicNotation}; + /// Concerned with querying information about a file's underlying inode. pub mod inode; use inode::{INodeError, Inode}; @@ -87,8 +90,12 @@ impl File { ) -> Result<Self, io::Error> { let path = data.path(); - let (symlink_target, metadata) = if *follow { - (fs::read_link(path).ok(), fs::metadata(path)?) + let (symlink_target, metadata) = if data.file_type().is_some_and(|ft| ft.is_symlink()) { + if *follow { + (fs::read_link(path).ok(), fs::metadata(path)?) + } else { + (fs::read_link(path).ok(), fs::symlink_metadata(path)?) + } } else { (None, fs::symlink_metadata(path)?) }; @@ -179,9 +186,49 @@ impl File { .map(|dt| format!("{dt}")) } + #[cfg(unix)] + pub fn is_fifo(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::Fifo) + } + + #[cfg(unix)] + pub fn is_socket(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::Socket) + } + + #[cfg(unix)] + pub fn is_char_device(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::CharDevice) + } + + #[cfg(unix)] + pub fn is_block_device(&self) -> bool { + self.metadata() + .permissions() + .try_mode_symbolic_notation() + .map_or(false, |mode| mode.file_type() == &FileType::BlockDevice) + } + pub fn is_dir(&self) -> bool { self.file_type().is_some_and(|ft| ft.is_dir()) } + + pub fn is_file(&self) -> bool { + self.file_type().is_some_and(|ft| ft.is_file()) + } + + pub fn is_symlink(&self) -> bool { + self.file_type().is_some_and(|ft| ft.is_symlink()) + } } impl Deref for File { diff --git a/src/file/tree/filter.rs b/src/file/tree/filter.rs new file mode 100644 index 0000000..f7e361c --- /dev/null +++ b/src/file/tree/filter.rs @@ -0,0 +1,61 @@ +use crate::{ + file::{tree::Tree, File}, + user::{ + args::{FileType, Layout}, + Context, + }, +}; +use ahash::HashSet; +use indextree::NodeId; + +/// Predicate used for filtering a [`File`] based on [`FileType`]. +pub type FileTypeFilter = dyn Fn(&File) -> bool; + +impl Tree { + /// Updates the [`Tree`]'s inner [`indextree::Arena`] to only contain files of certain + /// file-types. + pub fn filter_file_type( + &mut self, + Context { + layout, file_type, .. + }: &Context, + ) { + if file_type.is_empty() { + return; + } + + let mut filters = Vec::<Box<FileTypeFilter>>::new(); + + for ft in HashSet::from_iter(file_type) { + match ft { + FileType::Dir if matches!(layout, Layout::Tree | Layout::InvertedTree) => { + filters.push(Box::new(|f| f.is_dir())) + }, + FileType::Dir => filters.push(Box::new(|f| f.is_dir())), + FileType::File => filters.push(Box::new(|f| f.is_file())), + FileType::Symlink => filters.push(Box::new(|f| f.is_symlink())), + + #[cfg(unix)] + FileType::Pipe => filters.push(Box::new(|f| f.is_fifo())), + #[cfg(unix)] + FileType::Socket => filters.push(Box::new(|f| f.is_socket())), + #[cfg(unix)] + FileType::Char => filters.push(Box::new(|f| f.is_char_device())), + #[cfg(unix)] + FileType::Block => filters.push(Box::new(|f| f.is_block_device())), + } + } + + let no_match = |node_id: &NodeId| !filters.iter().any(|f| f(self.arena[*node_id].get())); + + let to_remove = self + .root_id + .descendants(&self.arena) + .filter(no_match) + .collect::<Vec<_>>(); + + to_remove + .into_iter() + .for_each(|n| n.detach(&mut self.arena)); + } +} diff --git a/src/file/tree/mod.rs b/src/file/tree/mod.rs index c805a27..bd25e21 100644 --- a/src/file/tree/mod.rs +++ b/src/file/tree/mod.rs @@ -11,7 +11,10 @@ use ahash::{HashMap, HashSet}; use indextree::{Arena, NodeId}; use std::{ops::Deref, path::PathBuf}; -/// Parallel disk reading +/// Concerned with filtering via file-type, globbing, and regular expressions. +mod filter; + +/// Parallel disk reading. mod traverse; /// Representation of the file-tree that is traversed starting from the root directory whose index diff --git a/src/file/tree/traverse.rs b/src/file/tree/traverse.rs index 4fd82ec..3ebbd54 100644 --- a/src/file/tree/traverse.rs +++ b/src/file/tree/traverse.rs @@ -121,7 +121,7 @@ mod walker { let walker = builder .follow_links(ctx.follow) .git_ignore(ctx.gitignore) - .git_global(ctx.gitignore) + .git_global(ctx.global_gitignore) .threads(ctx.threads) .hidden(ctx.no_hidden) .same_file_system(ctx.same_fs); diff --git a/src/main.rs b/src/main.rs index e3aa91f..402443c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,10 @@ fn run() -> Result<()> { file_tree.prune(); } + if !ctx.file_type.is_empty() { + file_tree.filter_file_type(&ctx) + } + let output = render::output(&file_tree, &ctx)?; let mut stdout = stdout().lock(); diff --git a/src/user/args.rs b/src/user/args.rs index 06990c0..7469fc3 100644 --- a/src/user/args.rs +++ b/src/user/args.rs @@ -131,10 +131,12 @@ pub enum Sort { /// Whether to sort directory entries relative either to their siblings or all directory entries #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum SortType { - /// Sort directory entries relative to their siblings + /// Sort directory entries relative to their siblings. Only applies when using 'tree' or + /// 'inverted-tree' layout. #[default] Tree, - /// Sort directory entries relative to all directory entries + /// Sort directory entries relative to all directory entries. Only applies when using 'flat' + /// layout. Flat, } @@ -151,3 +153,33 @@ pub enum DirOrder { /// Sort directories below files Last, } + +/// File-types found in both Unix and Windows. +#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default, Hash)] +pub enum FileType { + /// A regular file + #[default] + File, + + /// A directory + Dir, + + /// A symlink + Symlink, + + /// A FIFO + #[cfg(unix)] + Pipe, + + /// A socket + #[cfg(unix)] + Socket, + + /// A character device + #[cfg(unix)] + Char, + + /// A block device + #[cfg(unix)] + Block, +} diff --git a/src/user/mod.rs b/src/user/mod.rs index 86938ad..1a3dd30 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -30,18 +30,30 @@ pub struct Context { #[arg(long)] pub no_git: bool, - /// Ignore files in .gitignore - #[arg(short = 'i', long)] - pub gitignore: bool, - /// Report byte size in either binary or SI units #[arg(short, long, value_enum, default_value_t)] pub byte_units: args::BytePresentation, + /// Sort directories before or after all other file types + #[arg(short, long, value_enum, default_value_t)] + pub dir_order: args::DirOrder, + + /// Filter for specified file types + #[arg(short = 'F', long, value_enum)] + pub file_type: Vec<args::FileType>, + /// Follow symlinks #[arg(short = 'f', long)] pub follow: bool, + /// Ignore files that match rules in all .gitignore files encountered during traversal + #[arg(short = 'i', long)] + pub gitignore: bool, + + /// Ignore files that match rules in the global .gitignore file + #[arg(long)] + pub global_gitignore: bool, + /// Show extended metadata and attributes #[cfg(unix)] #[arg(short, long)] @@ -85,8 +97,12 @@ pub struct Context { #[arg(short, long, value_enum, default_value_t)] pub metric: args::Metric, - /// Omit empty directories from the output + /// Regular expression (or glob if '--glob' or '--iglob' is used) used to match files #[arg(short, long)] + pub pattern: Option<String>, + + /// Omit empty directories from the output + #[arg(short = 'P', long)] pub prune: bool, /// Field whereby to sort entries @@ -97,9 +113,9 @@ pub struct Context { #[arg(long, value_enum, default_value_t)] pub sort_type: args::SortType, - /// Sort directories before or after all other file types - #[arg(short, long, value_enum, default_value_t)] - pub dir_order: args::DirOrder, + /// Don't compute disk-usage and omit file size from output + #[arg(short = 'S', long)] + pub suppress_size: bool, /// Which kind of layout to use when rendering the output #[arg(short = 'y', long, value_enum, default_value_t)] @@ -113,10 +129,6 @@ pub struct Context { #[arg(short = 'x', long = "one-file-system")] pub same_fs: bool, - /// Don't compute disk-usage and omit file size from output - #[arg(long)] - pub suppress_size: bool, - /// Prints logs at the end of the output #[arg(short = 'v', long = "verbose")] pub verbose: bool, |