2016-04-17 19:38:37 +00:00
|
|
|
|
use std::cmp::Ordering;
|
|
|
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
|
|
|
|
|
|
use getopts;
|
2016-10-30 14:43:33 +00:00
|
|
|
|
use glob;
|
2016-04-17 19:38:37 +00:00
|
|
|
|
use natord;
|
|
|
|
|
|
|
|
|
|
use fs::File;
|
|
|
|
|
use options::misfire::Misfire;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The **file filter** processes a vector of files before outputting them,
|
|
|
|
|
/// filtering and sorting the files depending on the user’s command-line
|
|
|
|
|
/// flags.
|
2016-10-30 14:31:25 +00:00
|
|
|
|
#[derive(Default, PartialEq, Debug, Clone)]
|
2016-04-17 19:38:37 +00:00
|
|
|
|
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,
|
|
|
|
|
|
|
|
|
|
/// Whether to include invisible “dot” files 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.
|
|
|
|
|
///
|
|
|
|
|
/// This came about more or less by a complete historical accident,
|
|
|
|
|
/// when the original `ls` tried to hide `.` and `..`:
|
|
|
|
|
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
|
|
|
|
|
///
|
|
|
|
|
/// When one typed ls, however, these files appeared, so either Ken or
|
|
|
|
|
/// Dennis added a simple test to the program. It was in assembler then,
|
|
|
|
|
/// but the code in question was equivalent to something like this:
|
|
|
|
|
/// if (name[0] == '.') continue;
|
|
|
|
|
/// This statement was a little shorter than what it should have been,
|
|
|
|
|
/// which is:
|
|
|
|
|
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
|
|
|
|
/// but hey, it was easy.
|
|
|
|
|
///
|
|
|
|
|
/// Two things resulted.
|
|
|
|
|
///
|
|
|
|
|
/// First, a bad precedent was set. A lot of other lazy programmers
|
|
|
|
|
/// introduced bugs by making the same simplification. Actual files
|
|
|
|
|
/// beginning with periods are often skipped when they should be counted.
|
|
|
|
|
///
|
|
|
|
|
/// Second, and much worse, the idea of a "hidden" or "dot" file was
|
|
|
|
|
/// created. As a consequence, more lazy programmers started dropping
|
|
|
|
|
/// files into everyone's home directory. I don't have all that much
|
|
|
|
|
/// stuff installed on the machine I'm using to type this, but my home
|
|
|
|
|
/// directory has about a hundred dot files and I don't even know what
|
|
|
|
|
/// most of them are or whether they're still needed. Every file name
|
|
|
|
|
/// evaluation that goes through my home directory is slowed down by
|
|
|
|
|
/// this accumulated sludge.
|
2017-06-26 22:48:55 +00:00
|
|
|
|
pub dot_filter: DotFilter,
|
2016-10-30 14:43:33 +00:00
|
|
|
|
|
|
|
|
|
/// Glob patterns to ignore. Any file name that matches *any* of these
|
|
|
|
|
/// patterns won't be displayed in the list.
|
|
|
|
|
ignore_patterns: IgnorePatterns,
|
2016-04-17 19:38:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FileFilter {
|
|
|
|
|
|
|
|
|
|
/// Determines the set of file filter options to use, based on the user’s
|
|
|
|
|
/// command-line arguments.
|
|
|
|
|
pub fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
|
|
|
|
|
Ok(FileFilter {
|
|
|
|
|
list_dirs_first: matches.opt_present("group-directories-first"),
|
|
|
|
|
reverse: matches.opt_present("reverse"),
|
2017-03-26 16:35:50 +00:00
|
|
|
|
sort_field: SortField::deduce(matches)?,
|
2017-06-26 22:28:10 +00:00
|
|
|
|
dot_filter: DotFilter::deduce(matches),
|
2017-03-26 16:35:50 +00:00
|
|
|
|
ignore_patterns: IgnorePatterns::deduce(matches)?,
|
2016-04-17 19:38:37 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove every file in the given vector that does *not* pass the
|
2016-10-30 14:43:33 +00:00
|
|
|
|
/// filter predicate for files found inside a directory.
|
|
|
|
|
pub fn filter_child_files(&self, files: &mut Vec<File>) {
|
2017-06-26 22:28:10 +00:00
|
|
|
|
match self.dot_filter {
|
|
|
|
|
DotFilter::JustFiles => files.retain(|f| !f.is_dotfile()),
|
|
|
|
|
DotFilter::ShowDotfiles => {/* keep all elements */},
|
2017-06-26 22:48:55 +00:00
|
|
|
|
DotFilter::ShowDotfilesAndDots => unimplemented!(),
|
2016-04-17 19:38:37 +00:00
|
|
|
|
}
|
2016-10-30 14:43:33 +00:00
|
|
|
|
|
|
|
|
|
files.retain(|f| !self.ignore_patterns.is_ignored(f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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" shouldn't filter out the
|
|
|
|
|
/// dotfile, because it's 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!
|
|
|
|
|
pub fn filter_argument_files(&self, files: &mut Vec<File>) {
|
|
|
|
|
files.retain(|f| !self.ignore_patterns.is_ignored(f));
|
2016-04-17 19:38:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sort the files in the given vector based on the sort field option.
|
2016-06-11 12:35:31 +00:00
|
|
|
|
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
|
|
|
|
|
where F: AsRef<File<'a>> {
|
2016-04-17 19:38:37 +00:00
|
|
|
|
|
|
|
|
|
files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
|
|
|
|
|
|
|
|
|
|
if self.reverse {
|
|
|
|
|
files.reverse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.list_dirs_first {
|
|
|
|
|
// This relies on the fact that `sort_by` is stable.
|
|
|
|
|
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Compares two files to determine the order they should be listed in,
|
|
|
|
|
/// depending on the search field.
|
|
|
|
|
pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
|
|
|
|
|
use self::SortCase::{Sensitive, Insensitive};
|
|
|
|
|
|
|
|
|
|
match self.sort_field {
|
|
|
|
|
SortField::Unsorted => Ordering::Equal,
|
|
|
|
|
|
|
|
|
|
SortField::Name(Sensitive) => natord::compare(&a.name, &b.name),
|
|
|
|
|
SortField::Name(Insensitive) => natord::compare_ignore_case(&a.name, &b.name),
|
|
|
|
|
|
|
|
|
|
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
|
|
|
|
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
|
|
|
|
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
|
|
|
|
|
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
|
|
|
|
|
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
|
|
|
|
|
|
|
|
|
|
SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
|
|
|
|
|
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
|
|
|
|
order => order,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
|
|
|
|
|
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
|
|
|
|
|
order => order,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// User-supplied field to sort by.
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
|
pub enum SortField {
|
|
|
|
|
|
|
|
|
|
/// Don't apply any sorting. This is usually used as an optimisation in
|
|
|
|
|
/// scripts, where the order doesn't matter.
|
|
|
|
|
Unsorted,
|
|
|
|
|
|
|
|
|
|
/// The file name. This is the default sorting.
|
|
|
|
|
Name(SortCase),
|
|
|
|
|
|
|
|
|
|
/// The file's extension, with extensionless files being listed first.
|
|
|
|
|
Extension(SortCase),
|
|
|
|
|
|
|
|
|
|
/// The file's size.
|
|
|
|
|
Size,
|
|
|
|
|
|
|
|
|
|
/// The file's inode. This is sometimes analogous to the order in which
|
|
|
|
|
/// the files were created on the hard drive.
|
|
|
|
|
FileInode,
|
|
|
|
|
|
|
|
|
|
/// The time at which this 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,
|
|
|
|
|
|
|
|
|
|
/// The time at this 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 it’s read
|
|
|
|
|
/// slows the whole operation down, so many systems will only update the
|
|
|
|
|
/// timestamp in certain circumstances. This has become common enough that
|
|
|
|
|
/// it’s now expected behaviour for the `atime` field.
|
|
|
|
|
/// http://unix.stackexchange.com/a/8842
|
|
|
|
|
AccessedDate,
|
|
|
|
|
|
|
|
|
|
/// The time at which this file was changed or created (the `ctime`).
|
|
|
|
|
///
|
|
|
|
|
/// Contrary to the name, this field is used to mark the time when a
|
|
|
|
|
/// file's 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
|
|
|
|
|
CreatedDate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether a field should be sorted case-sensitively or case-insensitively.
|
|
|
|
|
///
|
|
|
|
|
/// This determines which of the `natord` functions to use.
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
|
pub enum SortCase {
|
|
|
|
|
|
|
|
|
|
/// Sort files case-sensitively with uppercase first, with ‘A’ coming
|
|
|
|
|
/// before ‘a’.
|
|
|
|
|
Sensitive,
|
|
|
|
|
|
|
|
|
|
/// Sort files case-insensitively, with ‘A’ being equal to ‘a’.
|
|
|
|
|
Insensitive,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for SortField {
|
|
|
|
|
fn default() -> SortField {
|
|
|
|
|
SortField::Name(SortCase::Sensitive)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SortField {
|
|
|
|
|
|
|
|
|
|
/// Determine the sort field to use, based on the presence of a “sort”
|
|
|
|
|
/// argument. This will return `Err` if the option is there, but does not
|
|
|
|
|
/// correspond to a valid field.
|
|
|
|
|
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
|
2017-06-23 21:58:07 +00:00
|
|
|
|
|
|
|
|
|
const SORTS: &[&str] = &[ "name", "Name", "size", "extension",
|
|
|
|
|
"Extension", "modified", "accessed",
|
|
|
|
|
"created", "inode", "none" ];
|
|
|
|
|
|
2016-04-17 19:38:37 +00:00
|
|
|
|
if let Some(word) = matches.opt_str("sort") {
|
|
|
|
|
match &*word {
|
|
|
|
|
"name" | "filename" => Ok(SortField::Name(SortCase::Sensitive)),
|
|
|
|
|
"Name" | "Filename" => Ok(SortField::Name(SortCase::Insensitive)),
|
|
|
|
|
"size" | "filesize" => Ok(SortField::Size),
|
|
|
|
|
"ext" | "extension" => Ok(SortField::Extension(SortCase::Sensitive)),
|
|
|
|
|
"Ext" | "Extension" => Ok(SortField::Extension(SortCase::Insensitive)),
|
|
|
|
|
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
|
|
|
|
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
|
|
|
|
"cr" | "created" => Ok(SortField::CreatedDate),
|
|
|
|
|
"none" => Ok(SortField::Unsorted),
|
|
|
|
|
"inode" => Ok(SortField::FileInode),
|
2017-06-23 21:58:07 +00:00
|
|
|
|
field => Err(Misfire::bad_argument("sort", field, SORTS))
|
2016-04-17 19:38:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
Ok(SortField::default())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-30 14:43:33 +00:00
|
|
|
|
|
|
|
|
|
|
2017-06-26 22:48:55 +00:00
|
|
|
|
/// Usually files in Unix use a leading dot to be hidden or visible, but two
|
|
|
|
|
/// entries in particular are "extra-hidden": `.` and `..`, which only become
|
|
|
|
|
/// visible after an extra `-a` option.
|
2017-06-26 22:28:10 +00:00
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2017-06-26 22:48:55 +00:00
|
|
|
|
pub enum DotFilter {
|
|
|
|
|
|
|
|
|
|
/// Shows files, dotfiles, and `.` and `..`.
|
|
|
|
|
ShowDotfilesAndDots,
|
2017-06-26 22:28:10 +00:00
|
|
|
|
|
|
|
|
|
/// Show files and dotfiles, but hide `.` and `..`.
|
|
|
|
|
ShowDotfiles,
|
|
|
|
|
|
|
|
|
|
/// Just show files, hiding anything beginning with a dot.
|
|
|
|
|
JustFiles,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for DotFilter {
|
|
|
|
|
fn default() -> DotFilter {
|
|
|
|
|
DotFilter::JustFiles
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl DotFilter {
|
|
|
|
|
pub fn deduce(matches: &getopts::Matches) -> DotFilter {
|
2017-06-26 22:48:55 +00:00
|
|
|
|
match matches.opt_count("all") {
|
|
|
|
|
0 => DotFilter::JustFiles,
|
|
|
|
|
1 => DotFilter::ShowDotfiles,
|
|
|
|
|
_ => DotFilter::ShowDotfilesAndDots,
|
|
|
|
|
}
|
2017-06-26 22:28:10 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2016-10-30 14:43:33 +00:00
|
|
|
|
#[derive(PartialEq, Default, Debug, Clone)]
|
|
|
|
|
struct IgnorePatterns {
|
|
|
|
|
patterns: Vec<glob::Pattern>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IgnorePatterns {
|
|
|
|
|
/// Determines the set of file filter options to use, based on the user’s
|
|
|
|
|
/// command-line arguments.
|
|
|
|
|
pub fn deduce(matches: &getopts::Matches) -> Result<IgnorePatterns, Misfire> {
|
|
|
|
|
let patterns = match matches.opt_str("ignore-glob") {
|
|
|
|
|
None => Ok(Vec::new()),
|
|
|
|
|
Some(is) => is.split('|').map(|a| glob::Pattern::new(a)).collect(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(IgnorePatterns {
|
2017-03-26 16:35:50 +00:00
|
|
|
|
patterns: patterns?,
|
2016-10-30 14:43:33 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_ignored(&self, file: &File) -> bool {
|
|
|
|
|
self.patterns.iter().any(|p| p.matches(&file.name))
|
|
|
|
|
}
|
|
|
|
|
}
|