exa/src/fs/filter.rs

374 lines
13 KiB
Rust
Raw Normal View History

2017-08-12 09:09:33 +00:00
//! Filtering and sorting the list of files before displaying them.
use std::cmp::Ordering;
use std::iter::FromIterator;
use std::os::unix::fs::MetadataExt;
2018-12-07 23:43:31 +00:00
use crate::fs::DotFilter;
use crate::fs::File;
2017-08-12 09:09:33 +00:00
/// The **file filter** processes a list of files before displaying them to
/// the user, by removing files they dont want to see, and putting the list
/// in the desired order.
///
/// Usually a user does not want to see *every* file in the list. The most
/// common case is to remove files starting with `.`, which are designated
/// as hidden files.
///
/// The special files `.` and `..` files are not actually filtered out, but
/// need to be inserted into the list, in a special case.
///
/// The filter also governs sorting the list. After being filtered, pairs of
/// files are compared and sorted based on the result, with the sort field
/// performing the comparison.
#[derive(PartialEq, Debug, Clone)]
pub struct FileFilter {
/// Whether directories should be listed first, and other types of file
/// second. Some users prefer it like this.
pub list_dirs_first: bool,
/// The metadata field to sort by.
pub sort_field: SortField,
/// Whether to reverse the sorting order. This would sort the largest
/// files first, or files starting with Z, or the most-recently-changed
/// ones, depending on the sort field.
pub reverse: bool,
2018-09-03 07:20:03 +00:00
/// Whether to only show directories.
pub only_dirs: bool,
/// Which invisible “dot” files to include when listing a directory.
///
/// Files starting with a single “.” are used to determine “system” or
/// “configuration” files that should not be displayed in a regular
/// directory listing, and the directory entries “.” and “..” are
/// considered extra-special.
///
/// This came about more or less by a complete historical accident,
/// when the original `ls` tried to hide `.` and `..`:
///
/// [Linux History: How Dot Files Became Hidden Files](https://linux-audit.com/linux-history-how-dot-files-became-hidden-files/)
pub dot_filter: DotFilter,
/// Glob patterns to ignore. Any file name that matches *any* of these
2017-08-12 09:09:33 +00:00
/// patterns wont be displayed in the list.
pub ignore_patterns: IgnorePatterns,
/// Whether to ignore Git-ignored patterns.
pub git_ignore: GitIgnore,
}
impl FileFilter {
2017-08-12 09:09:33 +00:00
/// Remove every file in the given vector that does *not* pass the
/// filter predicate for files found inside a directory.
2020-10-13 00:36:41 +00:00
pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| ! self.ignore_patterns.is_ignored(&f.name));
if self.only_dirs {
2020-10-10 14:57:40 +00:00
files.retain(File::is_directory);
}
2017-08-12 09:09:33 +00:00
}
/// Remove every file in the given vector that does *not* pass the
/// filter predicate for file names specified on the command-line.
///
/// The rules are different for these types of files than the other
/// type because the ignore rules can be used with globbing. For
/// example, running `exa -I='*.tmp' .vimrc` shouldnt filter out the
/// dotfile, because its been directly specified. But running
/// `exa -I='*.ogg' music/*` should filter out the ogg files obtained
/// from the glob, even though the globbing is done by the shell!
2020-10-13 00:36:41 +00:00
pub fn filter_argument_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| {
! self.ignore_patterns.is_ignored(&f.name)
});
2017-08-12 09:09:33 +00:00
}
/// Sort the files in the given vector based on the sort field option.
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
where F: AsRef<File<'a>>
{
files.sort_by(|a, b| {
self.sort_field.compare_files(a.as_ref(), b.as_ref())
});
2017-08-12 09:09:33 +00:00
if self.reverse {
files.reverse();
}
if self.list_dirs_first {
// This relies on the fact that `sort_by` is *stable*: it will keep
// adjacent elements next to each other.
files.sort_by(|a, b| {
b.as_ref().points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
2017-08-12 09:09:33 +00:00
}
}
}
/// User-supplied field to sort by.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortField {
2017-08-12 09:09:33 +00:00
/// Dont apply any sorting. This is usually used as an optimisation in
/// scripts, where the order doesnt matter.
Unsorted,
/// The file name. This is the default sorting.
Name(SortCase),
2017-08-12 09:09:33 +00:00
/// The files extension, with extensionless files being listed first.
Extension(SortCase),
2017-08-12 09:09:33 +00:00
/// The files size, in bytes.
Size,
2017-08-12 09:09:33 +00:00
/// The files inode, which usually corresponds to the order in which
/// files were created on the filesystem, more or less.
FileInode,
/// The time the file was modified (the “mtime”).
///
/// As this is stored as a Unix timestamp, rather than a local time
/// instance, the time zone does not matter and will only be used to
/// display the timestamps, not compare them.
ModifiedDate,
2017-09-28 15:14:36 +00:00
/// The time the file was accessed (the “atime”).
///
/// Oddly enough, this field rarely holds the *actual* accessed time.
/// Recording a read time means writing to the file each time its read
/// slows the whole operation down, so many systems will only update the
/// timestamp in certain circumstances. This has become common enough that
2017-08-12 09:09:33 +00:00
/// its now expected behaviour!
/// <https://unix.stackexchange.com/a/8842>
AccessedDate,
/// The time the file was changed (the “ctime”).
///
/// This field is used to mark the time when a files metadata
/// changed — its permissions, owners, or link count.
///
/// In original Unix, this was, however, meant as creation time.
/// <https://www.bell-labs.com/usr/dmr/www/cacm.html>
ChangedDate,
/// The time the file was created (the “btime” or “birthtime”).
CreatedDate,
/// The type of the file: directories, links, pipes, regular, files, etc.
///
/// Files are ordered according to the `PartialOrd` implementation of
/// `fs::fields::Type`, so changing that will change this.
FileType,
/// The “age” of the file, which is the time it was modified sorted
/// backwards. The reverse of the `ModifiedDate` ordering!
///
/// It turns out that listing the most-recently-modified files first is a
/// common-enough use case that it deserves its own variant. This would be
/// implemented by just using the modified date and setting the reverse
/// flag, but this would make reversing *that* output not work, which is
/// bad, even though thats kind of nonsensical. So its its own variant
/// that can be reversed like usual.
ModifiedAge,
/// The file's name, however if the name of the file begins with `.`
/// ignore the leading `.` and then sort as Name
NameMixHidden(SortCase),
}
/// Whether a field should be sorted case-sensitively or case-insensitively.
/// This determines which of the `natord` functions to use.
///
/// I kept on forgetting which one was sensitive and which one was
/// insensitive. Would a case-sensitive sort put capital letters first because
/// it takes the case of the letters into account, or intermingle them with
/// lowercase letters because it takes the difference between the two cases
/// into account? I gave up and just named these two variants after the
/// effects they have.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortCase {
/// Sort files case-sensitively with uppercase first, with A coming
/// before a.
ABCabc,
/// Sort files case-insensitively, with A being equal to a.
AaBbCc,
}
2017-08-12 09:09:33 +00:00
impl SortField {
/// Compares two files to determine the order they should be listed in,
/// depending on the search field.
///
/// The `natord` crate is used here to provide a more *natural* sorting
/// order than just sorting character-by-character. This splits filenames
/// into groups between letters and numbers, and then sorts those blocks
/// together, so `file10` will sort after `file9`, instead of before it
/// because of the `1`.
2020-10-13 00:36:41 +00:00
pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering {
use self::SortCase::{ABCabc, AaBbCc};
2017-08-12 09:09:33 +00:00
2018-06-19 12:58:03 +00:00
match self {
Self::Unsorted => Ordering::Equal,
2017-08-12 09:09:33 +00:00
Self::Name(ABCabc) => natord::compare(&a.name, &b.name),
Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
2017-08-12 09:09:33 +00:00
Self::Size => a.metadata.len().cmp(&b.metadata.len()),
Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()),
Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()),
Self::ChangedDate => a.changed_time().cmp(&b.changed_time()),
Self::CreatedDate => a.created_time().cmp(&b.created_time()),
Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a
2017-08-12 09:09:33 +00:00
Self::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes
2017-08-12 09:09:33 +00:00
Ordering::Equal => natord::compare(&*a.name, &*b.name),
order => order,
},
Self::Extension(ABCabc) => match a.ext.cmp(&b.ext) {
2017-08-12 09:09:33 +00:00
Ordering::Equal => natord::compare(&*a.name, &*b.name),
order => order,
},
Self::Extension(AaBbCc) => match a.ext.cmp(&b.ext) {
2017-08-12 09:09:33 +00:00
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
order => order,
},
Self::NameMixHidden(ABCabc) => natord::compare(
Self::strip_dot(&a.name),
Self::strip_dot(&b.name)
),
Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case(
Self::strip_dot(&a.name),
Self::strip_dot(&b.name)
)
}
}
fn strip_dot(n: &str) -> &str {
Cleanup clippy warnings warning: explicit lifetimes given in parameter types where they could be elided (or replaced with `'_` if needed by type declaration) --> src/output/escape.rs:4:1 | 4 | pub fn escape<'a>(string: String, bits: &mut Vec<ANSIString<'a>>, good: Style, bad: Style) { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | warning: this lifetime isn't used in the function definition --> src/output/escape.rs:4:15 | 4 | pub fn escape<'a>(string: String, bits: &mut Vec<ANSIString<'_>>, good: Style, bad: Style) { | ^^ | warning: single-character string constant used as pattern --> src/output/table.rs:310:41 | 310 | if file.starts_with(":") { | ^^^ help: try using a `char` instead: `':'` | warning: single-character string constant used as pattern --> src/output/table.rs:310:41 | 310 | if file.starts_with(":") { | ^^^ help: try using a `char` instead: `':'` | warning: methods called `new` usually return `Self` --> src/output/render/git.rs:38:5 | 38 | fn new(&self) -> Style; | ^^^^^^^^^^^^^^^^^^^^^^^ | warning: this lifetime isn't used in the function definition --> src/output/icons.rs:40:22 | 40 | pub fn iconify_style<'a>(style: Style) -> Style { | ^^ | warning: lint `clippy::find_map` has been removed: this lint has been replaced by `manual_find_map`, a more specific lint --> src/main.rs:11:10 | 11 | #![allow(clippy::find_map)] | ^^^^^^^^^^^^^^^^ | warning: redundant else block --> src/fs/dir.rs:124:18 | 124 | else { | __________________^ 125 | | return None 126 | | } | |_____________^ | warning: redundant else block --> src/options/view.rs:60:18 | 60 | else { | __________________^ 61 | | // the --tree case is handled by the DirAction parser later 62 | | return Ok(Self::Details(details)); 63 | | } | |_____________^ | warning: all variants have the same postfix: `Bytes` --> src/output/table.rs:170:1 | 170 | / pub enum SizeFormat { 171 | | 172 | | /// Format the file size using **decimal** prefixes, such as “kilo”, 173 | | /// “mega”, or “giga”. ... | 181 | | JustBytes, 182 | | } | |_^ | warning: all variants have the same postfix: `Bytes` --> src/output/table.rs:171:1 | 171 | / pub enum SizeFormat { 172 | | 173 | | /// Format the file size using **decimal** prefixes, such as “kilo”, 174 | | /// “mega”, or “giga”. ... | 182 | | JustBytes, 183 | | } | |_^ | warning: useless use of `format!` --> src/options/mod.rs:181:50 | 181 | return Err(OptionsError::Unsupported(format!( | __________________________________________________^ 182 | | "Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa" 183 | | ))); | |_____________^ help: consider using `.to_string()`: `"Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa".to_string()` | warning: stripping a prefix manually --> src/fs/filter.rs:287:33 | 287 | if n.starts_with('.') { &n[1..] } | ^^^^^^^ | warning: case-sensitive file extension comparison --> src/info/filetype.rs:24:19 | 24 | file.name.ends_with(".ninja") || | ^^^^^^^^^^^^^^^^^^^ |
2021-04-30 13:37:31 +00:00
match n.strip_prefix('.') {
Some(s) => s,
None => n,
}
2017-08-12 09:09:33 +00:00
}
}
/// The **ignore patterns** are a list of globs that are tested against
/// each filename, and if any of them match, that file isnt displayed.
/// This lets a user hide, say, text files by ignoring `*.txt`.
#[derive(PartialEq, Default, Debug, Clone)]
pub struct IgnorePatterns {
patterns: Vec<glob::Pattern>,
}
impl FromIterator<glob::Pattern> for IgnorePatterns {
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item = glob::Pattern>
{
let patterns = iter.into_iter().collect();
Self { patterns }
}
}
impl IgnorePatterns {
/// Create a new list from the input glob strings, turning the inputs that
2020-10-10 14:57:40 +00:00
/// are valid glob patterns into an `IgnorePatterns`. The inputs that
/// dont parse correctly are returned separately.
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(iter: I) -> (Self, Vec<glob::PatternError>) {
2017-08-12 09:09:33 +00:00
let iter = iter.into_iter();
// Almost all glob patterns are valid, so its worth pre-allocating
// the vector with enough space for all of them.
let mut patterns = match iter.size_hint() {
(_, Some(count)) => Vec::with_capacity(count),
_ => Vec::new(),
};
// Similarly, assume there wont be any errors.
let mut errors = Vec::new();
for input in iter {
match glob::Pattern::new(input) {
Ok(pat) => patterns.push(pat),
Err(e) => errors.push(e),
}
}
(Self { patterns }, errors)
}
2017-08-12 09:09:33 +00:00
/// Create a new empty set of patterns that matches nothing.
pub fn empty() -> Self {
Self { patterns: Vec::new() }
}
/// Test whether the given file should be hidden from the results.
2017-08-12 09:09:33 +00:00
fn is_ignored(&self, file: &str) -> bool {
self.patterns.iter().any(|p| p.matches(file))
}
}
/// Whether to ignore or display files that Git would ignore.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum GitIgnore {
/// Ignore files that Git would ignore.
CheckAndIgnore,
/// Display files, even if Git would ignore them.
Off,
}
2017-08-12 09:09:33 +00:00
#[cfg(test)]
mod test_ignores {
use super::*;
#[test]
fn empty_matches_nothing() {
let pats = IgnorePatterns::empty();
assert_eq!(false, pats.is_ignored("nothing"));
assert_eq!(false, pats.is_ignored("test.mp3"));
}
#[test]
fn ignores_a_glob() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]);
assert!(fails.is_empty());
assert_eq!(false, pats.is_ignored("nothing"));
assert_eq!(true, pats.is_ignored("test.mp3"));
}
#[test]
fn ignores_an_exact_filename() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]);
assert!(fails.is_empty());
assert_eq!(true, pats.is_ignored("nothing"));
assert_eq!(false, pats.is_ignored("test.mp3"));
}
#[test]
fn ignores_both() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]);
assert!(fails.is_empty());
assert_eq!(true, pats.is_ignored("nothing"));
assert_eq!(true, pats.is_ignored("test.mp3"));
}
}