summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Nguyen <benjamin.van.nguyen@gmail.com>2023-11-30 15:06:26 -0800
committerBenjamin Nguyen <benjamin.van.nguyen@gmail.com>2023-11-30 15:06:26 -0800
commit6a9e95b6df1b27bcc1f0a6874d9e5567ef3905e2 (patch)
treee102d0025fc46318fa155d9aaf34085083c680ac
parent867859b3ae2e3b6a7f68563477025bfd7bf2f1cf (diff)
filter by file type
-rw-r--r--.gitignore1
-rw-r--r--src/file/mod.rs51
-rw-r--r--src/file/tree/filter.rs61
-rw-r--r--src/file/tree/mod.rs5
-rw-r--r--src/file/tree/traverse.rs2
-rw-r--r--src/main.rs4
-rw-r--r--src/user/args.rs36
-rw-r--r--src/user/mod.rs36
8 files changed, 178 insertions, 18 deletions
diff --git a/.gitignore b/.gitignore
index 0592392..6bc848e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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,