options.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. use dir::Dir;
  2. use file::File;
  3. use column::Column;
  4. use column::Column::*;
  5. use output::{Grid, Details};
  6. use term::dimensions;
  7. use xattr;
  8. use std::cmp::Ordering;
  9. use std::fmt;
  10. use getopts;
  11. use natord;
  12. use datetime::local::{LocalDateTime, DatePiece};
  13. use self::Misfire::*;
  14. /// The *Options* struct represents a parsed version of the user's
  15. /// command-line options.
  16. #[derive(PartialEq, Debug, Copy)]
  17. pub struct Options {
  18. pub dir_action: DirAction,
  19. pub filter: FileFilter,
  20. pub view: View,
  21. }
  22. #[derive(PartialEq, Debug, Copy)]
  23. pub struct FileFilter {
  24. reverse: bool,
  25. show_invisibles: bool,
  26. sort_field: SortField,
  27. }
  28. #[derive(PartialEq, Debug, Copy)]
  29. pub enum View {
  30. Details(Details),
  31. Lines,
  32. Grid(Grid),
  33. }
  34. impl Options {
  35. /// Call getopts on the given slice of command-line strings.
  36. pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
  37. let mut opts = getopts::Options::new();
  38. if xattr::feature_implemented() {
  39. opts.optflag("@", "extended",
  40. "display extended attribute keys and sizes in long (-l) output"
  41. );
  42. }
  43. opts.optflag("1", "oneline", "display one entry per line");
  44. opts.optflag("a", "all", "show dot-files");
  45. opts.optflag("b", "binary", "use binary prefixes in file sizes");
  46. opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
  47. opts.optflag("d", "list-dirs", "list directories as regular files");
  48. opts.optflag("g", "group", "show group as well as user");
  49. opts.optflag("h", "header", "show a header row at the top");
  50. opts.optflag("H", "links", "show number of hard links");
  51. opts.optflag("i", "inode", "show each file's inode number");
  52. opts.optflag("l", "long", "display extended details and attributes");
  53. opts.optflag("m", "modified", "display timestamp of most recent modification");
  54. opts.optflag("r", "reverse", "reverse order of files");
  55. opts.optflag("R", "recurse", "recurse into directories");
  56. opts.optopt ("s", "sort", "field to sort by", "WORD");
  57. opts.optflag("S", "blocks", "show number of file system blocks");
  58. opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
  59. opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
  60. opts.optflag("u", "accessed", "display timestamp of last access for a file");
  61. opts.optflag("U", "created", "display timestamp of creation for a file");
  62. opts.optflag("x", "across", "sort multi-column view entries across");
  63. opts.optflag("?", "help", "show list of command-line options");
  64. let matches = match opts.parse(args) {
  65. Ok(m) => m,
  66. Err(e) => return Err(Misfire::InvalidOptions(e)),
  67. };
  68. if matches.opt_present("help") {
  69. return Err(Misfire::Help(opts.usage("Usage:\n exa [options] [files...]")));
  70. }
  71. let sort_field = match matches.opt_str("sort") {
  72. Some(word) => try!(SortField::from_word(word)),
  73. None => SortField::Name,
  74. };
  75. let filter = FileFilter {
  76. reverse: matches.opt_present("reverse"),
  77. show_invisibles: matches.opt_present("all"),
  78. sort_field: sort_field,
  79. };
  80. let path_strs = if matches.free.is_empty() {
  81. vec![ ".".to_string() ]
  82. }
  83. else {
  84. matches.free.clone()
  85. };
  86. Ok((Options {
  87. dir_action: try!(DirAction::deduce(&matches)),
  88. view: try!(View::deduce(&matches, filter)),
  89. filter: filter,
  90. }, path_strs))
  91. }
  92. pub fn transform_files<'a>(&self, files: &mut Vec<File<'a>>) {
  93. self.filter.transform_files(files)
  94. }
  95. }
  96. impl FileFilter {
  97. /// Transform the files (sorting, reversing, filtering) before listing them.
  98. pub fn transform_files<'a>(&self, files: &mut Vec<File<'a>>) {
  99. if !self.show_invisibles {
  100. files.retain(|f| !f.is_dotfile());
  101. }
  102. match self.sort_field {
  103. SortField::Unsorted => {},
  104. SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
  105. SortField::Size => files.sort_by(|a, b| a.stat.size.cmp(&b.stat.size)),
  106. SortField::FileInode => files.sort_by(|a, b| a.stat.unstable.inode.cmp(&b.stat.unstable.inode)),
  107. SortField::Extension => files.sort_by(|a, b| match a.ext.cmp(&b.ext) {
  108. Ordering::Equal => natord::compare(&*a.name, &*b.name),
  109. order => order
  110. }),
  111. SortField::ModifiedDate => files.sort_by(|a, b| a.stat.modified.cmp(&b.stat.modified)),
  112. SortField::AccessedDate => files.sort_by(|a, b| a.stat.accessed.cmp(&b.stat.accessed)),
  113. SortField::CreatedDate => files.sort_by(|a, b| a.stat.created.cmp(&b.stat.created)),
  114. }
  115. if self.reverse {
  116. files.reverse();
  117. }
  118. }
  119. }
  120. /// User-supplied field to sort by.
  121. #[derive(PartialEq, Debug, Copy)]
  122. pub enum SortField {
  123. Unsorted, Name, Extension, Size, FileInode,
  124. ModifiedDate, AccessedDate, CreatedDate,
  125. }
  126. impl SortField {
  127. /// Find which field to use based on a user-supplied word.
  128. fn from_word(word: String) -> Result<SortField, Misfire> {
  129. match &word[..] {
  130. "name" | "filename" => Ok(SortField::Name),
  131. "size" | "filesize" => Ok(SortField::Size),
  132. "ext" | "extension" => Ok(SortField::Extension),
  133. "mod" | "modified" => Ok(SortField::ModifiedDate),
  134. "acc" | "accessed" => Ok(SortField::AccessedDate),
  135. "cr" | "created" => Ok(SortField::CreatedDate),
  136. "none" => Ok(SortField::Unsorted),
  137. "inode" => Ok(SortField::FileInode),
  138. field => Err(SortField::none(field))
  139. }
  140. }
  141. /// How to display an error when the word didn't match with anything.
  142. fn none(field: &str) -> Misfire {
  143. Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
  144. }
  145. }
  146. /// One of these things could happen instead of listing files.
  147. #[derive(PartialEq, Debug)]
  148. pub enum Misfire {
  149. /// The getopts crate didn't like these arguments.
  150. InvalidOptions(getopts::Fail),
  151. /// The user asked for help. This isn't strictly an error, which is why
  152. /// this enum isn't named Error!
  153. Help(String),
  154. /// Two options were given that conflict with one another
  155. Conflict(&'static str, &'static str),
  156. /// An option was given that does nothing when another one either is or
  157. /// isn't present.
  158. Useless(&'static str, bool, &'static str),
  159. }
  160. impl Misfire {
  161. /// The OS return code this misfire should signify.
  162. pub fn error_code(&self) -> i32 {
  163. if let Help(_) = *self { 2 }
  164. else { 3 }
  165. }
  166. }
  167. impl fmt::Display for Misfire {
  168. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  169. match *self {
  170. InvalidOptions(ref e) => write!(f, "{}", e),
  171. Help(ref text) => write!(f, "{}", text),
  172. Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
  173. Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
  174. Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b),
  175. }
  176. }
  177. }
  178. impl View {
  179. pub fn deduce(matches: &getopts::Matches, filter: FileFilter) -> Result<View, Misfire> {
  180. if matches.opt_present("long") {
  181. if matches.opt_present("across") {
  182. Err(Misfire::Useless("across", true, "long"))
  183. }
  184. else if matches.opt_present("oneline") {
  185. Err(Misfire::Useless("oneline", true, "long"))
  186. }
  187. else {
  188. let details = Details {
  189. columns: try!(Columns::deduce(matches)),
  190. header: matches.opt_present("header"),
  191. tree: matches.opt_present("recurse"),
  192. xattr: xattr::feature_implemented() && matches.opt_present("extended"),
  193. filter: filter,
  194. };
  195. Ok(View::Details(details))
  196. }
  197. }
  198. else if matches.opt_present("binary") {
  199. Err(Misfire::Useless("binary", false, "long"))
  200. }
  201. else if matches.opt_present("bytes") {
  202. Err(Misfire::Useless("bytes", false, "long"))
  203. }
  204. else if matches.opt_present("inode") {
  205. Err(Misfire::Useless("inode", false, "long"))
  206. }
  207. else if matches.opt_present("links") {
  208. Err(Misfire::Useless("links", false, "long"))
  209. }
  210. else if matches.opt_present("header") {
  211. Err(Misfire::Useless("header", false, "long"))
  212. }
  213. else if matches.opt_present("blocks") {
  214. Err(Misfire::Useless("blocks", false, "long"))
  215. }
  216. else if matches.opt_present("time") {
  217. Err(Misfire::Useless("time", false, "long"))
  218. }
  219. else if matches.opt_present("tree") {
  220. Err(Misfire::Useless("tree", false, "long"))
  221. }
  222. else if xattr::feature_implemented() && matches.opt_present("extended") {
  223. Err(Misfire::Useless("extended", false, "long"))
  224. }
  225. else if matches.opt_present("oneline") {
  226. if matches.opt_present("across") {
  227. Err(Misfire::Useless("across", true, "oneline"))
  228. }
  229. else {
  230. Ok(View::Lines)
  231. }
  232. }
  233. else {
  234. if let Some((width, _)) = dimensions() {
  235. let grid = Grid {
  236. across: matches.opt_present("across"),
  237. console_width: width
  238. };
  239. Ok(View::Grid(grid))
  240. }
  241. else {
  242. // If the terminal width couldn't be matched for some reason, such
  243. // as the program's stdout being connected to a file, then
  244. // fallback to the lines view.
  245. Ok(View::Lines)
  246. }
  247. }
  248. }
  249. }
  250. #[derive(PartialEq, Debug, Copy)]
  251. pub enum SizeFormat {
  252. DecimalBytes,
  253. BinaryBytes,
  254. JustBytes,
  255. }
  256. impl SizeFormat {
  257. pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
  258. let binary = matches.opt_present("binary");
  259. let bytes = matches.opt_present("bytes");
  260. match (binary, bytes) {
  261. (true, true ) => Err(Misfire::Conflict("binary", "bytes")),
  262. (true, false) => Ok(SizeFormat::BinaryBytes),
  263. (false, true ) => Ok(SizeFormat::JustBytes),
  264. (false, false) => Ok(SizeFormat::DecimalBytes),
  265. }
  266. }
  267. }
  268. #[derive(PartialEq, Debug, Copy)]
  269. pub enum TimeType {
  270. FileAccessed,
  271. FileModified,
  272. FileCreated,
  273. }
  274. impl TimeType {
  275. pub fn header(&self) -> &'static str {
  276. match *self {
  277. TimeType::FileAccessed => "Date Accessed",
  278. TimeType::FileModified => "Date Modified",
  279. TimeType::FileCreated => "Date Created",
  280. }
  281. }
  282. }
  283. #[derive(PartialEq, Debug, Copy)]
  284. pub struct TimeTypes {
  285. accessed: bool,
  286. modified: bool,
  287. created: bool,
  288. }
  289. impl TimeTypes {
  290. /// Find which field to use based on a user-supplied word.
  291. fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
  292. let possible_word = matches.opt_str("time");
  293. let modified = matches.opt_present("modified");
  294. let created = matches.opt_present("created");
  295. let accessed = matches.opt_present("accessed");
  296. if let Some(word) = possible_word {
  297. if modified {
  298. return Err(Misfire::Useless("modified", true, "time"));
  299. }
  300. else if created {
  301. return Err(Misfire::Useless("created", true, "time"));
  302. }
  303. else if accessed {
  304. return Err(Misfire::Useless("accessed", true, "time"));
  305. }
  306. match &word[..] {
  307. "mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
  308. "acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
  309. "cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
  310. field => Err(TimeTypes::none(field)),
  311. }
  312. }
  313. else {
  314. if modified || created || accessed {
  315. Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
  316. }
  317. else {
  318. Ok(TimeTypes { accessed: false, modified: true, created: false })
  319. }
  320. }
  321. }
  322. /// How to display an error when the word didn't match with anything.
  323. fn none(field: &str) -> Misfire {
  324. Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field)))
  325. }
  326. }
  327. /// What to do when encountering a directory?
  328. #[derive(PartialEq, Debug, Copy)]
  329. pub enum DirAction {
  330. AsFile, List, Recurse, Tree
  331. }
  332. impl DirAction {
  333. pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
  334. let recurse = matches.opt_present("recurse");
  335. let list = matches.opt_present("list-dirs");
  336. let tree = matches.opt_present("tree");
  337. match (recurse, list, tree) {
  338. (false, _, true ) => Err(Misfire::Useless("tree", false, "recurse")),
  339. (true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
  340. (true, false, false) => Ok(DirAction::Recurse),
  341. (true, false, true ) => Ok(DirAction::Tree),
  342. (false, true, _ ) => Ok(DirAction::AsFile),
  343. (false, false, _ ) => Ok(DirAction::List),
  344. }
  345. }
  346. }
  347. #[derive(PartialEq, Copy, Debug)]
  348. pub struct Columns {
  349. size_format: SizeFormat,
  350. time_types: TimeTypes,
  351. inode: bool,
  352. links: bool,
  353. blocks: bool,
  354. group: bool,
  355. }
  356. impl Columns {
  357. pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
  358. Ok(Columns {
  359. size_format: try!(SizeFormat::deduce(matches)),
  360. time_types: try!(TimeTypes::deduce(matches)),
  361. inode: matches.opt_present("inode"),
  362. links: matches.opt_present("links"),
  363. blocks: matches.opt_present("blocks"),
  364. group: matches.opt_present("group"),
  365. })
  366. }
  367. pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
  368. let mut columns = vec![];
  369. if self.inode {
  370. columns.push(Inode);
  371. }
  372. columns.push(Permissions);
  373. if self.links {
  374. columns.push(HardLinks);
  375. }
  376. columns.push(FileSize(self.size_format));
  377. if self.blocks {
  378. columns.push(Blocks);
  379. }
  380. columns.push(User);
  381. if self.group {
  382. columns.push(Group);
  383. }
  384. let current_year = LocalDateTime::now().year();
  385. if self.time_types.modified {
  386. columns.push(Timestamp(TimeType::FileModified, current_year));
  387. }
  388. if self.time_types.created {
  389. columns.push(Timestamp(TimeType::FileCreated, current_year));
  390. }
  391. if self.time_types.accessed {
  392. columns.push(Timestamp(TimeType::FileAccessed, current_year));
  393. }
  394. if cfg!(feature="git") {
  395. if let Some(d) = dir {
  396. if d.has_git_repo() {
  397. columns.push(GitStatus);
  398. }
  399. }
  400. }
  401. columns
  402. }
  403. }
  404. #[cfg(test)]
  405. mod test {
  406. use super::Options;
  407. use super::Misfire;
  408. use super::Misfire::*;
  409. use xattr;
  410. fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
  411. match misfire {
  412. Err(Help(_)) => true,
  413. _ => false,
  414. }
  415. }
  416. #[test]
  417. fn help() {
  418. let opts = Options::getopts(&[ "--help".to_string() ]);
  419. assert!(is_helpful(opts))
  420. }
  421. #[test]
  422. fn help_with_file() {
  423. let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
  424. assert!(is_helpful(opts))
  425. }
  426. #[test]
  427. fn files() {
  428. let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
  429. assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
  430. }
  431. #[test]
  432. fn no_args() {
  433. let args = Options::getopts(&[]).unwrap().1;
  434. assert_eq!(args, vec![ ".".to_string() ])
  435. }
  436. #[test]
  437. fn file_sizes() {
  438. let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
  439. assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
  440. }
  441. #[test]
  442. fn just_binary() {
  443. let opts = Options::getopts(&[ "--binary".to_string() ]);
  444. assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
  445. }
  446. #[test]
  447. fn just_bytes() {
  448. let opts = Options::getopts(&[ "--bytes".to_string() ]);
  449. assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
  450. }
  451. #[test]
  452. fn long_across() {
  453. let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
  454. assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
  455. }
  456. #[test]
  457. fn oneline_across() {
  458. let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
  459. assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
  460. }
  461. #[test]
  462. fn just_header() {
  463. let opts = Options::getopts(&[ "--header".to_string() ]);
  464. assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
  465. }
  466. #[test]
  467. fn just_inode() {
  468. let opts = Options::getopts(&[ "--inode".to_string() ]);
  469. assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
  470. }
  471. #[test]
  472. fn just_links() {
  473. let opts = Options::getopts(&[ "--links".to_string() ]);
  474. assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
  475. }
  476. #[test]
  477. fn just_blocks() {
  478. let opts = Options::getopts(&[ "--blocks".to_string() ]);
  479. assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
  480. }
  481. #[test]
  482. fn extended_without_long() {
  483. if xattr::feature_implemented() {
  484. let opts = Options::getopts(&[ "--extended".to_string() ]);
  485. assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
  486. }
  487. }
  488. #[test]
  489. fn tree_without_recurse() {
  490. let opts = Options::getopts(&[ "--tree".to_string() ]);
  491. assert_eq!(opts.unwrap_err(), Misfire::Useless("tree", false, "recurse"))
  492. }
  493. }