| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- //! Parsing the options for `FileFilter`.
- use crate::fs::DotFilter;
- use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
- use crate::options::{flags, OptionsError};
- use crate::options::parser::MatchedFlags;
- impl FileFilter {
- /// Determines which of all the file filter options to use.
- pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
- 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 {
- /// Determines which sort field to use based on the `--sort` argument.
- /// This argument’s value can be one of several flags, listed above.
- /// Returns the default sort field if none is given, or `Err` if the
- /// value doesn’t correspond to a sort field we know about.
- fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
- let Some(word) = matches.get(&flags::SORT)? else { return Ok(Self::default()) };
- // Get String because we can’t match an OsStr
- let Some(word) = word.to_str() else { return Err(OptionsError::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, doesn’t 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
- }
- #[cfg(unix)]
- "inode" => {
- Self::FileInode
- }
- "type" => {
- Self::FileType
- }
- "none" => {
- Self::Unsorted
- }
- _ => {
- return Err(OptionsError::BadArgument(&flags::SORT, word.into()));
- }
- };
- Ok(field)
- }
- }
- // I’ve gone back and forth between whether to sort case-sensitively or
- // insensitively by default. The default string sort in most programming
- // languages takes each character’s ASCII value into account, sorting
- // “Documents” before “apps”, but there’s usually an option to ignore
- // characters’ case, putting “apps” before “Documents”.
- //
- // The argument for following case is that it’s 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 it’s not in, it looks like it’s 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 directory’s 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 wouldn’t
- // 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 number’s
- // smaller, because that’s 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.
- //
- // In the same way, users will name their files with letters expecting the
- // order of the letters to matter, rather than each letter’s character’s ASCII
- // 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`.
- impl Default for SortField {
- fn default() -> Self {
- Self::Name(SortCase::AaBbCc)
- }
- }
- impl DotFilter {
- /// Determines the dot filter based on how many `--all` options were
- /// given: one will show dotfiles, but two will show `.` and `..` too.
- /// --almost-all is equivalent to --all, included for compatibility with
- /// `ls -A`.
- ///
- /// It also checks for the `--tree` option, because of a special case
- /// where `--tree --all --all` won’t work: listing the parent directory
- /// in tree mode would loop onto itself!
- ///
- /// `--almost-all` binds stronger than multiple `--all` as we currently do not take the order
- /// of arguments into account and it is the safer option (does not clash with `--tree`)
- pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
- let all_count = matches.count(&flags::ALL);
- let has_almost_all = matches.has(&flags::ALMOST_ALL)?;
- match (all_count, has_almost_all) {
- (0, false) => Ok(Self::JustFiles),
- // either a single --all or at least one --almost-all is given
- (1, _) | (0, true) => Ok(Self::Dotfiles),
- // more than one --all
- (c, _) => if matches.count(&flags::TREE) > 0 {
- Err(OptionsError::TreeAllAll)
- } else if matches.is_strict() && c > 2 {
- Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))
- } else {
- Ok(Self::DotfilesAndDots)
- },
- }
- }
- }
- impl IgnorePatterns {
- /// Determines the set of glob patterns to use based on the
- /// `--ignore-glob` argument’s value. This is a list of strings
- /// separated by pipe (`|`) characters, given in any order.
- pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
- // If there are no inputs, we return a set of patterns that doesn’t
- // match anything, rather than, say, `None`.
- let Some(inputs) = matches.get(&flags::IGNORE_GLOB)? else { return Ok(Self::empty()) };
- // 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,
- // but we only use one. (TODO)
- match errors.pop() {
- Some(e) => Err(e.into()),
- None => Ok(patterns),
- }
- }
- }
- impl GitIgnore {
- pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
- if matches.has(&flags::GIT_IGNORE)? {
- Ok(Self::CheckAndIgnore)
- }
- else {
- Ok(Self::Off)
- }
- }
- }
- #[cfg(test)]
- mod test {
- use super::*;
- use std::ffi::OsString;
- use crate::options::flags;
- use crate::options::parser::Flag;
- macro_rules! test {
- ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
- #[test]
- fn $name() {
- use crate::options::parser::Arg;
- use crate::options::test::parse_for_test;
- use crate::options::test::Strictnesses::*;
- static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::ALMOST_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);
- }
- }
- };
- }
- mod sort_fields {
- use super::*;
- // Default behaviour
- test!(empty: SortField <- []; Both => Ok(SortField::default()));
- // Sort field arguments
- 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));
- 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)));
- // Errors
- test!(error: SortField <- ["--sort=colour"]; Both => Err(OptionsError::BadArgument(&flags::SORT, OsString::from("colour"))));
- // 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(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
- test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
- }
- mod dot_filters {
- use super::*;
- // Default behaviour
- test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles));
- // --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(OptionsError::Conflict(&flags::ALL, &flags::ALL)));
- // --all and --tree
- test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles));
- test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll));
- test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll));
- // --almost-all
- test!(almost_all: DotFilter <- ["--almost-all"]; Both => Ok(DotFilter::Dotfiles));
- test!(almost_all_all: DotFilter <- ["-Aa"]; Both => Ok(DotFilter::Dotfiles));
- test!(almost_all_all_2: DotFilter <- ["-Aaa"]; Both => Ok(DotFilter::DotfilesAndDots));
- }
- mod ignore_patterns {
- use super::*;
- use std::iter::FromIterator;
- 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(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
- test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
- }
- mod git_ignores {
- use super::*;
- test!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off));
- test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore));
- }
- }
|