filter.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. //! Parsing the options for `FileFilter`.
  2. use fs::DotFilter;
  3. use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns};
  4. use options::{flags, Misfire};
  5. use options::parser::MatchedFlags;
  6. impl FileFilter {
  7. /// Determines which of all the file filter options to use.
  8. pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> {
  9. Ok(FileFilter {
  10. list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
  11. reverse: matches.has(&flags::REVERSE)?,
  12. sort_field: SortField::deduce(matches)?,
  13. dot_filter: DotFilter::deduce(matches)?,
  14. ignore_patterns: IgnorePatterns::deduce(matches)?,
  15. })
  16. }
  17. }
  18. impl SortField {
  19. /// Determines which sort field to use based on the `--sort` argument.
  20. /// This argument’s value can be one of several flags, listed above.
  21. /// Returns the default sort field if none is given, or `Err` if the
  22. /// value doesn’t correspond to a sort field we know about.
  23. fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
  24. let word = match matches.get(&flags::SORT)? {
  25. Some(w) => w,
  26. None => return Ok(SortField::default()),
  27. };
  28. // The field is an OsStr, so can’t be matched.
  29. if word == "name" || word == "filename" {
  30. Ok(SortField::Name(SortCase::AaBbCc))
  31. }
  32. else if word == "Name" || word == "Filename" {
  33. Ok(SortField::Name(SortCase::ABCabc))
  34. }
  35. else if word == "size" || word == "filesize" {
  36. Ok(SortField::Size)
  37. }
  38. else if word == "ext" || word == "extension" {
  39. Ok(SortField::Extension(SortCase::AaBbCc))
  40. }
  41. else if word == "Ext" || word == "Extension" {
  42. Ok(SortField::Extension(SortCase::ABCabc))
  43. }
  44. else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "old" || word == "oldest" {
  45. Ok(SortField::ModifiedDate)
  46. }
  47. else if word == "age" || word == "new" || word == "newest" {
  48. Ok(SortField::ModifiedAge)
  49. }
  50. else if word == "acc" || word == "accessed" {
  51. Ok(SortField::AccessedDate)
  52. }
  53. else if word == "cr" || word == "created" {
  54. Ok(SortField::CreatedDate)
  55. }
  56. else if word == "inode" {
  57. Ok(SortField::FileInode)
  58. }
  59. else if word == "type" {
  60. Ok(SortField::FileType)
  61. }
  62. else if word == "none" {
  63. Ok(SortField::Unsorted)
  64. }
  65. else {
  66. Err(Misfire::BadArgument(&flags::SORT, word.into()))
  67. }
  68. }
  69. }
  70. // I’ve gone back and forth between whether to sort case-sensitively or
  71. // insensitively by default. The default string sort in most programming
  72. // languages takes each character’s ASCII value into account, sorting
  73. // “Documents” before “apps”, but there’s usually an option to ignore
  74. // characters’ case, putting “apps” before “Documents”.
  75. //
  76. // The argument for following case is that it’s easy to forget whether an item
  77. // begins with an uppercase or lowercase letter and end up having to scan both
  78. // the uppercase and lowercase sub-lists to find the item you want. If you
  79. // happen to pick the sublist it’s not in, it looks like it’s missing, which
  80. // is worse than if you just take longer to find it.
  81. // (https://ux.stackexchange.com/a/79266)
  82. //
  83. // The argument for ignoring case is that it makes exa sort files differently
  84. // from shells. A user would expect a directory’s files to be in the same
  85. // order if they used “exa ~/directory” or “exa ~/directory/*”, but exa sorts
  86. // them in the first case, and the shell in the second case, so they wouldn’t
  87. // be exactly the same if exa does something non-conventional.
  88. //
  89. // However, exa already sorts files differently: it uses natural sorting from
  90. // the natord crate, sorting the string “2” before “10” because the number’s
  91. // smaller, because that’s usually what the user expects to happen. Users will
  92. // name their files with numbers expecting them to be treated like numbers,
  93. // rather than lists of numeric characters.
  94. //
  95. // In the same way, users will name their files with letters expecting the
  96. // order of the letters to matter, rather than each letter’s character’s ASCII
  97. // value. So exa breaks from tradition and ignores case while sorting:
  98. // “apps” first, then “Documents”.
  99. //
  100. // You can get the old behaviour back by sorting with `--sort=Name`.
  101. impl Default for SortField {
  102. fn default() -> SortField {
  103. SortField::Name(SortCase::AaBbCc)
  104. }
  105. }
  106. impl DotFilter {
  107. /// Determines the dot filter based on how many `--all` options were
  108. /// given: one will show dotfiles, but two will show `.` and `..` too.
  109. ///
  110. /// It also checks for the `--tree` option in strict mode, because of a
  111. /// special case where `--tree --all --all` won’t work: listing the
  112. /// parent directory in tree mode would loop onto itself!
  113. pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> {
  114. let count = matches.count(&flags::ALL);
  115. if count == 0 {
  116. Ok(DotFilter::JustFiles)
  117. }
  118. else if count == 1 {
  119. Ok(DotFilter::Dotfiles)
  120. }
  121. else if matches.count(&flags::TREE) > 0 {
  122. Err(Misfire::TreeAllAll)
  123. }
  124. else if count >= 3 && matches.is_strict() {
  125. Err(Misfire::Conflict(&flags::ALL, &flags::ALL))
  126. }
  127. else {
  128. Ok(DotFilter::DotfilesAndDots)
  129. }
  130. }
  131. }
  132. impl IgnorePatterns {
  133. /// Determines the set of glob patterns to use based on the
  134. /// `--ignore-patterns` argument’s value. This is a list of strings
  135. /// separated by pipe (`|`) characters, given in any order.
  136. pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> {
  137. // If there are no inputs, we return a set of patterns that doesn’t
  138. // match anything, rather than, say, `None`.
  139. let inputs = match matches.get(&flags::IGNORE_GLOB)? {
  140. None => return Ok(IgnorePatterns::empty()),
  141. Some(is) => is,
  142. };
  143. // Awkwardly, though, a glob pattern can be invalid, and we need to
  144. // deal with invalid patterns somehow.
  145. let (patterns, mut errors) = IgnorePatterns::parse_from_iter(inputs.to_string_lossy().split('|'));
  146. // It can actually return more than one glob error,
  147. // but we only use one. (TODO)
  148. match errors.pop() {
  149. Some(e) => Err(e.into()),
  150. None => Ok(patterns),
  151. }
  152. }
  153. }
  154. #[cfg(test)]
  155. mod test {
  156. use super::*;
  157. use std::ffi::OsString;
  158. use options::flags;
  159. use options::parser::Flag;
  160. macro_rules! test {
  161. ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
  162. #[test]
  163. fn $name() {
  164. use options::parser::Arg;
  165. use options::test::parse_for_test;
  166. use options::test::Strictnesses::*;
  167. static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB ];
  168. for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
  169. assert_eq!(result, $result);
  170. }
  171. }
  172. };
  173. }
  174. mod sort_fields {
  175. use super::*;
  176. // Default behaviour
  177. test!(empty: SortField <- []; Both => Ok(SortField::default()));
  178. // Sort field arguments
  179. test!(one_arg: SortField <- ["--sort=cr"]; Both => Ok(SortField::CreatedDate));
  180. test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size));
  181. test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
  182. test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc)));
  183. test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc)));
  184. test!(old: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedDate));
  185. test!(oldest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedDate));
  186. test!(new: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedAge));
  187. test!(newest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedAge));
  188. test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge));
  189. // Errors
  190. test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::BadArgument(&flags::SORT, OsString::from("colour"))));
  191. // Overriding
  192. test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
  193. test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc)));
  194. test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
  195. test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
  196. }
  197. mod dot_filters {
  198. use super::*;
  199. // Default behaviour
  200. test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles));
  201. // --all
  202. test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles));
  203. test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots));
  204. test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots));
  205. test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots));
  206. test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(Misfire::Conflict(&flags::ALL, &flags::ALL)));
  207. // --all and --tree
  208. test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles));
  209. test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(Misfire::TreeAllAll));
  210. test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(Misfire::TreeAllAll));
  211. }
  212. mod ignore_patternses {
  213. use super::*;
  214. use std::iter::FromIterator;
  215. use glob;
  216. fn pat(string: &'static str) -> glob::Pattern {
  217. glob::Pattern::new(string).unwrap()
  218. }
  219. // Various numbers of globs
  220. test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
  221. test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
  222. test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
  223. test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
  224. // Overriding
  225. test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ])));
  226. test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ])));
  227. test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
  228. test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
  229. }
  230. }