Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(filter): --cachedir-ignore option #949

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ eza’s options are almost, but not quite, entirely unlike `ls`’s. Quick overv
- **-f**, **--only-files**: list only files
- **--git-ignore**: ignore files mentioned in `.gitignore`
- **-I**, **--ignore-glob=(globs)**: glob patterns (pipe-separated) of files to ignore
- **--cachedir-ignore**: Ignore directories with a `CACHEDIR.TAG` file.

Pass the `--all` option twice to also show the `.` and `..` directories.

Expand Down
1 change: 1 addition & 0 deletions completions/fish/eza.fish
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ complete -c eza -l smart-group -d "Only show group if it has a different name fr
# Filtering and sorting options
complete -c eza -l group-directories-first -d "Sort directories before other files"
complete -c eza -l git-ignore -d "Ignore files mentioned in '.gitignore'"
complete -c eza -l cachedir-ignore -d "Ignore directories with a 'CACHEDIR.TAG' file"
complete -c eza -s a -l all -d "Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories"
complete -c eza -s A -l almost-all -d "Equivalent to --all; included for compatibility with `ls -A`"
complete -c eza -s d -l list-dirs -d "List directories like regular 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 @@ -21,6 +21,7 @@ export extern "eza" [
--absolute # Display entries with their absolute path
--group-directories-first # Sort directories before other files
--git-ignore # Ignore files mentioned in '.gitignore'
--cachedir-ignore # Ignore directories with a 'CACHEDIR.TAG' file
--all(-a) # Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories
--almost-all(-A) # Equivalent to --all; included for compatibility with `ls -A`
--list-dirs(-d) # List directories like regular files
Expand Down
1 change: 1 addition & 0 deletions completions/zsh/_eza
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ __eza() {
--absolute"[Display entries with their absolute path]:(mode):(on follow off)" \
--group-directories-first"[Sort directories before other files]" \
--git-ignore"[Ignore files mentioned in '.gitignore']" \
--ignore-cachedir"[Ignore directories with a 'CACHEDIR.TAG' file]" \
{-a,--all}"[Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories]" \
{-A,--almost-all}"[Equivalent to --all; included for compatibility with \'ls -A\']" \
{-d,--list-dirs}"[List directories like regular files]" \
Expand Down
3 changes: 3 additions & 0 deletions man/eza.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ Sort fields starting with a capital letter will sort uppercase before lowercase:
`-I`, `--ignore-glob=GLOBS`
: Glob patterns, pipe-separated, of files to ignore.

`--ignore-cachedir`
: Ignore directories with a 'CACHEDIR.TAG' file.

`--git-ignore` [if eza was built with git support]
: Do not list files that are ignored by Git.

Expand Down
64 changes: 64 additions & 0 deletions src/fs/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use std::os::unix::fs::MetadataExt;
use crate::fs::DotFilter;
use crate::fs::File;

/// magic number of CACHEDIR.TAG files
const CACHEDIR_MAGIC: &[u8; 43] = b"Signature: 8a477f597d28d172789f06886806bc55";

/// Flags used to manage the **file filter** process
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum FileFilterFlags {
Expand Down Expand Up @@ -68,6 +71,10 @@ pub struct FileFilter {

/// Whether to ignore Git-ignored patterns.
pub git_ignore: GitIgnore,

/// Whether to ignore CACHEDIR.TAG directories.
/// see also https://bford.info/cachedir/
pub ignore_cachedir: IgnoreCacheDir,
}

impl FileFilter {
Expand All @@ -77,6 +84,7 @@ impl FileFilter {
use FileFilterFlags::{OnlyDirs, OnlyFiles};

files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
self.filter_cachedirs(files);

match (
self.flags.contains(&OnlyDirs),
Expand All @@ -94,6 +102,53 @@ impl FileFilter {
}
}

/// remove directories if they contain a CACHEDIR.TAG with the correct signature.
/// does nothing if `self.cachedir_ignore == Off`
pub fn filter_cachedirs(&self, files: &mut Vec<File<'_>>) {
if self.ignore_cachedir == IgnoreCacheDir::CheckAndIgnore {
files.retain(|f| {
if !f.is_directory() {
return true;
}
let Ok(read_dir) = std::fs::read_dir(&f.path) else {
return true;
};
let mut found_tag = false;
for child in read_dir {
let Ok(child) = child else {
continue;
};
let is_cachedir_tag = Self::is_cachedir_tag(&child.path());
if is_cachedir_tag {
found_tag = true;
break;
}
}
!found_tag
});
}
}

/// check if `path` is named "CACHEDIR.TAG" and has the correct magic number ([`CACHEDIR_MAGIC`]).
fn is_cachedir_tag(path: &std::path::Path) -> bool {
use std::ffi::OsStr;
use std::io::Read;
if path.file_name() != Some(OsStr::new("CACHEDIR.TAG")) {
return false;
}
if path.is_symlink() {
return false;
}
let Ok(mut reader) = std::fs::File::open(path) else {
return false;
};
let mut buf = [0u8; 43];
let Ok(()) = reader.read_exact(&mut buf) else {
return false;
};
&buf == CACHEDIR_MAGIC
}

/// Remove every file in the given vector that does *not* pass the
/// filter predicate for file names specified on the command-line.
///
Expand Down Expand Up @@ -354,6 +409,15 @@ pub enum GitIgnore {
Off,
}

/// Whether to ignore or display files that Git would ignore.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum IgnoreCacheDir {
/// Ignore directories with CACHEDIR.TAG and have the correct signature
CheckAndIgnore,
/// Do not check for CACHEDIR.TAG
Off,
}

#[cfg(test)]
mod test_ignores {
use super::*;
Expand Down
13 changes: 12 additions & 1 deletion src/options/filter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Parsing the options for `FileFilter`.

use crate::fs::filter::{
FileFilter, FileFilterFlags, GitIgnore, IgnorePatterns, SortCase, SortField,
FileFilter, FileFilterFlags, GitIgnore, IgnoreCacheDir, IgnorePatterns, SortCase, SortField,
};
use crate::fs::DotFilter;

Expand Down Expand Up @@ -32,6 +32,7 @@ impl FileFilter {
dot_filter: DotFilter::deduce(matches)?,
ignore_patterns: IgnorePatterns::deduce(matches)?,
git_ignore: GitIgnore::deduce(matches)?,
ignore_cachedir: IgnoreCacheDir::deduce(matches)?,
});
}
}
Expand Down Expand Up @@ -193,6 +194,16 @@ impl GitIgnore {
}
}

impl IgnoreCacheDir {
pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
if matches.has(&flags::IGNORE_CACHEDIR)? {
Ok(Self::CheckAndIgnore)
} else {
Ok(Self::Off)
}
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
25 changes: 13 additions & 12 deletions src/options/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,18 @@ const SCALES: Values = &["all", "size", "age"];
const COLOR_SCALE_MODES: Values = &["fixed", "gradient"];

// filtering and sorting options
pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden };
pub static ALMOST_ALL: Arg = Arg { short: Some(b'A'), long: "almost-all", takes_value: TakesValue::Forbidden };
pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden };
pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary(None) };
pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden };
pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary(Some(SORTS)) };
pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary(None) };
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 ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden };
pub static ALMOST_ALL: Arg = Arg { short: Some(b'A'), long: "almost-all", takes_value: TakesValue::Forbidden };
pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden };
pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary(None) };
pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden };
pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary(Some(SORTS)) };
pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary(None) };
pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", takes_value: TakesValue::Forbidden };
pub static IGNORE_CACHEDIR: Arg = Arg { short: None, long: "ignore-cachedir", 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 };
const SORTS: Values = &[ "name", "Name", "size", "extension",
"Extension", "modified", "changed", "accessed",
"created", "inode", "type", "none" ];
Expand Down Expand Up @@ -93,7 +94,7 @@ pub static ALL_ARGS: Args = Args(&[
&WIDTH, &NO_QUOTES, &ABSOLUTE,

&ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
&IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES,
&IGNORE_GLOB, &GIT_IGNORE, &IGNORE_CACHEDIR, &ONLY_DIRS, &ONLY_FILES,

&BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
&BLOCKSIZE, &TOTAL_SIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS,
Expand Down
5 changes: 3 additions & 2 deletions src/options/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ FILTERING AND SORTING OPTIONS
--group-directories-first list directories before other files
-D, --only-dirs list only directories
-f, --only-files list only files
-I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore";
-I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore
--cachedir-ignore ignore directories with a 'CACHEDIR.TAG' file";

static GIT_FILTER_HELP: &str = " \
--git-ignore ignore files mentioned in '.gitignore'";
Expand Down Expand Up @@ -78,7 +79,7 @@ LONG VIEW OPTIONS
--no-filesize suppress the filesize field
--no-user suppress the user field
--no-time suppress the time field
--stdin read file names from stdin, one per line or other separator
--stdin read file names from stdin, one per line or other separator
specified in environment";

static GIT_VIEW_HELP: &str = " \
Expand Down