filter.rs 13 KB

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