exa/src/options/filter.rs

324 lines
13 KiB
Rust
Raw Normal View History

2017-08-12 09:09:33 +00:00
//! Parsing the options for `FileFilter`.
use crate::fs::DotFilter;
2018-12-07 23:43:31 +00:00
use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
2018-12-07 23:43:31 +00:00
use crate::options::{flags, Misfire};
use crate::options::parser::MatchedFlags;
impl FileFilter {
2017-08-12 09:09:33 +00:00
/// Determines which of all the file filter options to use.
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
Ok(Self {
list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
reverse: matches.has(&flags::REVERSE)?,
only_dirs: matches.has(&flags::ONLY_DIRS)?,
sort_field: SortField::deduce(matches)?,
dot_filter: DotFilter::deduce(matches)?,
ignore_patterns: IgnorePatterns::deduce(matches)?,
git_ignore: GitIgnore::deduce(matches)?,
})
}
}
impl SortField {
2017-08-12 09:09:33 +00:00
/// Determines which sort field to use based on the `--sort` argument.
/// This arguments value can be one of several flags, listed above.
/// Returns the default sort field if none is given, or `Err` if the
/// value doesnt correspond to a sort field we know about.
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
let word = match matches.get(&flags::SORT)? {
Some(w) => w,
None => return Ok(Self::default()),
};
// Get String because we cant match an OsStr
let word = match word.to_str() {
Some(w) => w,
None => return Err(Misfire::BadArgument(&flags::SORT, word.into()))
};
let field = match word {
"name" | "filename" => {
Self::Name(SortCase::AaBbCc)
}
"Name" | "Filename" => {
Self::Name(SortCase::ABCabc)
}
".name" | ".filename" => {
Self::NameMixHidden(SortCase::AaBbCc)
}
".Name" | ".Filename" => {
Self::NameMixHidden(SortCase::ABCabc)
}
"size" | "filesize" => {
Self::Size
}
"ext" | "extension" => {
Self::Extension(SortCase::AaBbCc)
}
"Ext" | "Extension" => {
Self::Extension(SortCase::ABCabc)
}
// “new” sorts oldest at the top and newest at the bottom; “old”
// sorts newest at the top and oldest at the bottom. I think this
// is the right way round to do this: “size” puts the smallest at
// the top and the largest at the bottom, doesnt it?
"date" | "time" | "mod" | "modified" | "new" | "newest" => {
Self::ModifiedDate
}
// Similarly, “age” means that files with the least age (the
// newest files) get sorted at the top, and files with the most
// age (the oldest) at the bottom.
"age" | "old" | "oldest" => {
Self::ModifiedAge
}
"ch" | "changed" => {
Self::ChangedDate
}
"acc" | "accessed" => {
Self::AccessedDate
}
"cr" | "created" => {
Self::CreatedDate
}
"inode" => {
Self::FileInode
}
"type" => {
Self::FileType
}
"none" => {
Self::Unsorted
}
_ => {
return Err(Misfire::BadArgument(&flags::SORT, word.into()));
}
};
Ok(field)
}
}
// Ive gone back and forth between whether to sort case-sensitively or
// insensitively by default. The default string sort in most programming
// languages takes each characters ASCII value into account, sorting
// “Documents” before “apps”, but theres usually an option to ignore
// characters case, putting “apps” before “Documents”.
//
// The argument for following case is that its easy to forget whether an item
// begins with an uppercase or lowercase letter and end up having to scan both
// the uppercase and lowercase sub-lists to find the item you want. If you
// happen to pick the sublist its not in, it looks like its missing, which
// is worse than if you just take longer to find it.
// (https://ux.stackexchange.com/a/79266)
//
// The argument for ignoring case is that it makes exa sort files differently
// from shells. A user would expect a directorys files to be in the same
// order if they used “exa ~/directory” or “exa ~/directory/*”, but exa sorts
// them in the first case, and the shell in the second case, so they wouldnt
// be exactly the same if exa does something non-conventional.
//
// However, exa already sorts files differently: it uses natural sorting from
// the natord crate, sorting the string “2” before “10” because the numbers
// smaller, because thats usually what the user expects to happen. Users will
// name their files with numbers expecting them to be treated like numbers,
// rather than lists of numeric characters.
2017-08-26 22:53:47 +00:00
//
// In the same way, users will name their files with letters expecting the
// order of the letters to matter, rather than each letters characters ASCII
2017-08-26 22:53:47 +00:00
// value. So exa breaks from tradition and ignores case while sorting:
// “apps” first, then “Documents”.
//
// You can get the old behaviour back by sorting with `--sort=Name`.
2017-08-12 09:09:33 +00:00
impl Default for SortField {
fn default() -> Self {
Self::Name(SortCase::AaBbCc)
2017-08-12 09:09:33 +00:00
}
}
impl DotFilter {
2017-08-12 09:09:33 +00:00
/// Determines the dot filter based on how many `--all` options were
/// given: one will show dotfiles, but two will show `.` and `..` too.
///
/// It also checks for the `--tree` option in strict mode, because of a
/// special case where `--tree --all --all` wont work: listing the
2017-08-12 09:09:33 +00:00
/// parent directory in tree mode would loop onto itself!
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
let count = matches.count(&flags::ALL);
if count == 0 {
Ok(Self::JustFiles)
}
else if count == 1 {
Ok(Self::Dotfiles)
}
else if matches.count(&flags::TREE) > 0 {
Err(Misfire::TreeAllAll)
}
else if count >= 3 && matches.is_strict() {
Err(Misfire::Conflict(&flags::ALL, &flags::ALL))
}
else {
Ok(Self::DotfilesAndDots)
}
}
}
impl IgnorePatterns {
2017-08-12 09:09:33 +00:00
/// Determines the set of glob patterns to use based on the
2020-04-19 03:53:17 +00:00
/// `--ignore-glob` arguments value. This is a list of strings
2017-08-12 09:09:33 +00:00
/// separated by pipe (`|`) characters, given in any order.
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
2017-08-12 09:09:33 +00:00
// If there are no inputs, we return a set of patterns that doesnt
// match anything, rather than, say, `None`.
let inputs = match matches.get(&flags::IGNORE_GLOB)? {
Some(is) => is,
None => return Ok(Self::empty()),
};
2017-08-12 09:09:33 +00:00
// Awkwardly, though, a glob pattern can be invalid, and we need to
// deal with invalid patterns somehow.
let (patterns, mut errors) = Self::parse_from_iter(inputs.to_string_lossy().split('|'));
// It can actually return more than one glob error,
2017-08-12 09:09:33 +00:00
// but we only use one. (TODO)
match errors.pop() {
Some(e) => Err(e.into()),
None => Ok(patterns),
}
}
}
2017-07-26 19:45:01 +00:00
impl GitIgnore {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
if matches.has(&flags::GIT_IGNORE)? {
Ok(Self::CheckAndIgnore)
}
else {
Ok(Self::Off)
}
}
}
2017-07-26 19:45:01 +00:00
#[cfg(test)]
mod test {
use super::*;
use std::ffi::OsString;
2018-12-07 23:43:31 +00:00
use crate::options::flags;
use crate::options::parser::Flag;
2017-07-26 19:45:01 +00:00
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
2017-07-26 19:45:01 +00:00
#[test]
fn $name() {
2018-12-07 23:43:31 +00:00
use crate::options::parser::Arg;
use crate::options::test::parse_for_test;
use crate::options::test::Strictnesses::*;
2017-07-26 19:45:01 +00:00
static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
assert_eq!(result, $result);
}
2017-07-26 19:45:01 +00:00
}
};
}
mod sort_fields {
use super::*;
// Default behaviour
test!(empty: SortField <- []; Both => Ok(SortField::default()));
2017-07-26 19:45:01 +00:00
// Sort field arguments
2018-12-17 10:05:25 +00:00
test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate));
test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size));
test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc)));
test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc)));
test!(old: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedDate));
test!(oldest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedDate));
test!(new: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedAge));
test!(newest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedAge));
test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge));
2017-07-26 19:45:01 +00:00
test!(mix_hidden_lowercase: SortField <- ["--sort", ".name"]; Both => Ok(SortField::NameMixHidden(SortCase::AaBbCc)));
test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc)));
2017-07-26 19:45:01 +00:00
// Errors
test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::BadArgument(&flags::SORT, OsString::from("colour"))));
2017-07-26 19:45:01 +00:00
// Overriding
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc)));
test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
2017-07-26 19:45:01 +00:00
}
2017-07-26 20:01:22 +00:00
mod dot_filters {
use super::*;
// Default behaviour
test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles));
2017-07-26 20:01:22 +00:00
// --all
test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles));
test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots));
test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots));
test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots));
test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(Misfire::Conflict(&flags::ALL, &flags::ALL)));
2017-07-26 20:01:22 +00:00
// --all and --tree
test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles));
test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(Misfire::TreeAllAll));
test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(Misfire::TreeAllAll));
2017-07-26 20:01:22 +00:00
}
2017-07-26 20:29:49 +00:00
2017-10-31 05:24:31 +00:00
mod ignore_patterns {
2017-07-26 20:29:49 +00:00
use super::*;
use std::iter::FromIterator;
2017-07-26 20:29:49 +00:00
use glob;
fn pat(string: &'static str) -> glob::Pattern {
glob::Pattern::new(string).unwrap()
}
// Various numbers of globs
test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
// Overriding
test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ])));
test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ])));
test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
2017-07-26 20:29:49 +00:00
}
mod git_ignores {
use super::*;
test!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off));
test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore));
}
2017-07-26 19:45:01 +00:00
}