options.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. use std::cmp;
  2. use std::default;
  3. use std::fmt;
  4. use std::num::ParseIntError;
  5. use std::os::unix::fs::MetadataExt;
  6. use getopts;
  7. use natord;
  8. use colours::Colours;
  9. use column::Column;
  10. use column::Column::*;
  11. use dir::Dir;
  12. use feature::xattr;
  13. use file::File;
  14. use output::{Grid, Details, GridDetails, Lines};
  15. use term::dimensions;
  16. /// The *Options* struct represents a parsed version of the user's
  17. /// command-line options.
  18. #[derive(PartialEq, Debug, Copy, Clone)]
  19. pub struct Options {
  20. pub dir_action: DirAction,
  21. pub filter: FileFilter,
  22. pub view: View,
  23. }
  24. impl Options {
  25. /// Call getopts on the given slice of command-line strings.
  26. pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
  27. let mut opts = getopts::Options::new();
  28. opts.optflag("1", "oneline", "display one entry per line");
  29. opts.optflag("a", "all", "show dot-files");
  30. opts.optflag("b", "binary", "use binary prefixes in file sizes");
  31. opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
  32. opts.optflag("d", "list-dirs", "list directories as regular files");
  33. opts.optflag("g", "group", "show group as well as user");
  34. opts.optflag("G", "grid", "display entries in a grid view (default)");
  35. opts.optflag("", "group-directories-first", "list directories before other files");
  36. opts.optflag("h", "header", "show a header row at the top");
  37. opts.optflag("H", "links", "show number of hard links");
  38. opts.optflag("i", "inode", "show each file's inode number");
  39. opts.optflag("l", "long", "display extended details and attributes");
  40. opts.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
  41. opts.optflag("m", "modified", "display timestamp of most recent modification");
  42. opts.optflag("r", "reverse", "reverse order of files");
  43. opts.optflag("R", "recurse", "recurse into directories");
  44. opts.optopt ("s", "sort", "field to sort by", "WORD");
  45. opts.optflag("S", "blocks", "show number of file system blocks");
  46. opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
  47. opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
  48. opts.optflag("u", "accessed", "display timestamp of last access for a file");
  49. opts.optflag("U", "created", "display timestamp of creation for a file");
  50. opts.optflag("x", "across", "sort multi-column view entries across");
  51. opts.optflag("", "version", "display version of exa");
  52. opts.optflag("?", "help", "show list of command-line options");
  53. if cfg!(feature="git") {
  54. opts.optflag("", "git", "show git status");
  55. }
  56. if xattr::ENABLED {
  57. opts.optflag("@", "extended", "display extended attribute keys and sizes in long (-l) output");
  58. }
  59. let matches = match opts.parse(args) {
  60. Ok(m) => m,
  61. Err(e) => return Err(Misfire::InvalidOptions(e)),
  62. };
  63. if matches.opt_present("help") {
  64. return Err(Misfire::Help(opts.usage("Usage:\n exa [options] [files...]")));
  65. }
  66. else if matches.opt_present("version") {
  67. return Err(Misfire::Version);
  68. }
  69. let sort_field = match matches.opt_str("sort") {
  70. Some(word) => try!(SortField::from_word(word)),
  71. None => SortField::default(),
  72. };
  73. let filter = FileFilter {
  74. list_dirs_first: matches.opt_present("group-directories-first"),
  75. reverse: matches.opt_present("reverse"),
  76. show_invisibles: matches.opt_present("all"),
  77. sort_field: sort_field,
  78. };
  79. let path_strs = if matches.free.is_empty() {
  80. vec![ ".".to_string() ]
  81. }
  82. else {
  83. matches.free.clone()
  84. };
  85. let dir_action = try!(DirAction::deduce(&matches));
  86. let view = try!(View::deduce(&matches, filter, dir_action));
  87. Ok((Options {
  88. dir_action: dir_action,
  89. view: view,
  90. filter: filter,
  91. }, path_strs))
  92. }
  93. pub fn transform_files(&self, files: &mut Vec<File>) {
  94. self.filter.transform_files(files)
  95. }
  96. /// Whether the View specified in this set of options includes a Git
  97. /// status column. It's only worth trying to discover a repository if the
  98. /// results will end up being displayed.
  99. pub fn should_scan_for_git(&self) -> bool {
  100. match self.view {
  101. View::Details(Details { columns: Some(cols), .. }) => cols.should_scan_for_git(),
  102. View::GridDetails(GridDetails { details: Details { columns: Some(cols), .. }, .. }) => cols.should_scan_for_git(),
  103. _ => false,
  104. }
  105. }
  106. }
  107. #[derive(PartialEq, Debug, Copy, Clone)]
  108. pub struct FileFilter {
  109. list_dirs_first: bool,
  110. reverse: bool,
  111. show_invisibles: bool,
  112. sort_field: SortField,
  113. }
  114. impl FileFilter {
  115. /// Transform the files (sorting, reversing, filtering) before listing them.
  116. pub fn transform_files(&self, files: &mut Vec<File>) {
  117. if !self.show_invisibles {
  118. files.retain(|f| !f.is_dotfile());
  119. }
  120. match self.sort_field {
  121. SortField::Unsorted => {},
  122. SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
  123. SortField::Size => files.sort_by(|a, b| a.metadata.len().cmp(&b.metadata.len())),
  124. SortField::FileInode => files.sort_by(|a, b| a.metadata.ino().cmp(&b.metadata.ino())),
  125. SortField::ModifiedDate => files.sort_by(|a, b| a.metadata.mtime().cmp(&b.metadata.mtime())),
  126. SortField::AccessedDate => files.sort_by(|a, b| a.metadata.atime().cmp(&b.metadata.atime())),
  127. SortField::CreatedDate => files.sort_by(|a, b| a.metadata.ctime().cmp(&b.metadata.ctime())),
  128. SortField::Extension => files.sort_by(|a, b| match a.ext.cmp(&b.ext) {
  129. cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
  130. order => order,
  131. }),
  132. }
  133. if self.reverse {
  134. files.reverse();
  135. }
  136. if self.list_dirs_first {
  137. // This relies on the fact that sort_by is stable.
  138. files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
  139. }
  140. }
  141. }
  142. /// User-supplied field to sort by.
  143. #[derive(PartialEq, Debug, Copy, Clone)]
  144. pub enum SortField {
  145. Unsorted, Name, Extension, Size, FileInode,
  146. ModifiedDate, AccessedDate, CreatedDate,
  147. }
  148. impl default::Default for SortField {
  149. fn default() -> SortField {
  150. SortField::Name
  151. }
  152. }
  153. impl SortField {
  154. /// Find which field to use based on a user-supplied word.
  155. fn from_word(word: String) -> Result<SortField, Misfire> {
  156. match &word[..] {
  157. "name" | "filename" => Ok(SortField::Name),
  158. "size" | "filesize" => Ok(SortField::Size),
  159. "ext" | "extension" => Ok(SortField::Extension),
  160. "mod" | "modified" => Ok(SortField::ModifiedDate),
  161. "acc" | "accessed" => Ok(SortField::AccessedDate),
  162. "cr" | "created" => Ok(SortField::CreatedDate),
  163. "none" => Ok(SortField::Unsorted),
  164. "inode" => Ok(SortField::FileInode),
  165. field => Err(SortField::none(field))
  166. }
  167. }
  168. /// How to display an error when the word didn't match with anything.
  169. fn none(field: &str) -> Misfire {
  170. Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
  171. }
  172. }
  173. /// One of these things could happen instead of listing files.
  174. #[derive(PartialEq, Debug)]
  175. pub enum Misfire {
  176. /// The getopts crate didn't like these arguments.
  177. InvalidOptions(getopts::Fail),
  178. /// The user asked for help. This isn't strictly an error, which is why
  179. /// this enum isn't named Error!
  180. Help(String),
  181. /// The user wanted the version number.
  182. Version,
  183. /// Two options were given that conflict with one another.
  184. Conflict(&'static str, &'static str),
  185. /// An option was given that does nothing when another one either is or
  186. /// isn't present.
  187. Useless(&'static str, bool, &'static str),
  188. /// An option was given that does nothing when either of two other options
  189. /// are not present.
  190. Useless2(&'static str, &'static str, &'static str),
  191. /// A numeric option was given that failed to be parsed as a number.
  192. FailedParse(ParseIntError),
  193. }
  194. impl Misfire {
  195. /// The OS return code this misfire should signify.
  196. pub fn error_code(&self) -> i32 {
  197. if let Misfire::Help(_) = *self { 2 }
  198. else { 3 }
  199. }
  200. }
  201. impl fmt::Display for Misfire {
  202. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  203. use self::Misfire::*;
  204. match *self {
  205. InvalidOptions(ref e) => write!(f, "{}", e),
  206. Help(ref text) => write!(f, "{}", text),
  207. Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
  208. Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
  209. Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
  210. Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b),
  211. Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
  212. FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
  213. }
  214. }
  215. }
  216. #[derive(PartialEq, Debug, Copy, Clone)]
  217. pub enum View {
  218. Details(Details),
  219. Grid(Grid),
  220. GridDetails(GridDetails),
  221. Lines(Lines),
  222. }
  223. impl View {
  224. pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
  225. use self::Misfire::*;
  226. let long = || {
  227. if matches.opt_present("across") && !matches.opt_present("grid") {
  228. Err(Useless("across", true, "long"))
  229. }
  230. else if matches.opt_present("oneline") {
  231. Err(Useless("oneline", true, "long"))
  232. }
  233. else {
  234. let details = Details {
  235. columns: Some(try!(Columns::deduce(matches))),
  236. header: matches.opt_present("header"),
  237. recurse: dir_action.recurse_options().map(|o| (o, filter)),
  238. xattr: xattr::ENABLED && matches.opt_present("extended"),
  239. colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
  240. };
  241. Ok(details)
  242. }
  243. };
  244. let long_options_scan = || {
  245. for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
  246. if matches.opt_present(option) {
  247. return Err(Useless(option, false, "long"));
  248. }
  249. }
  250. if cfg!(feature="git") && matches.opt_present("git") {
  251. Err(Useless("git", false, "long"))
  252. }
  253. else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
  254. Err(Useless2("level", "recurse", "tree"))
  255. }
  256. else if xattr::ENABLED && matches.opt_present("extended") {
  257. Err(Useless("extended", false, "long"))
  258. }
  259. else {
  260. Ok(())
  261. }
  262. };
  263. let other_options_scan = || {
  264. if let Some((width, _)) = dimensions() {
  265. if matches.opt_present("oneline") {
  266. if matches.opt_present("across") {
  267. Err(Useless("across", true, "oneline"))
  268. }
  269. else {
  270. let lines = Lines {
  271. colours: Colours::colourful(),
  272. };
  273. Ok(View::Lines(lines))
  274. }
  275. }
  276. else if matches.opt_present("tree") {
  277. let details = Details {
  278. columns: None,
  279. header: false,
  280. recurse: dir_action.recurse_options().map(|o| (o, filter)),
  281. xattr: false,
  282. colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
  283. };
  284. Ok(View::Details(details))
  285. }
  286. else {
  287. let grid = Grid {
  288. across: matches.opt_present("across"),
  289. console_width: width,
  290. colours: Colours::colourful(),
  291. };
  292. Ok(View::Grid(grid))
  293. }
  294. }
  295. else {
  296. // If the terminal width couldn't be matched for some reason, such
  297. // as the program's stdout being connected to a file, then
  298. // fallback to the lines view.
  299. let lines = Lines {
  300. colours: Colours::plain(),
  301. };
  302. Ok(View::Lines(lines))
  303. }
  304. };
  305. if matches.opt_present("long") {
  306. let long_options = try!(long());
  307. if matches.opt_present("grid") {
  308. match other_options_scan() {
  309. Ok(View::Grid(grid)) => return Ok(View::GridDetails(GridDetails { grid: grid, details: long_options })),
  310. Ok(lines) => return Ok(lines),
  311. Err(e) => return Err(e),
  312. };
  313. }
  314. else {
  315. return Ok(View::Details(long_options));
  316. }
  317. }
  318. try!(long_options_scan());
  319. other_options_scan()
  320. }
  321. }
  322. #[derive(PartialEq, Debug, Copy, Clone)]
  323. pub enum SizeFormat {
  324. DecimalBytes,
  325. BinaryBytes,
  326. JustBytes,
  327. }
  328. impl default::Default for SizeFormat {
  329. fn default() -> SizeFormat {
  330. SizeFormat::DecimalBytes
  331. }
  332. }
  333. impl SizeFormat {
  334. pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
  335. let binary = matches.opt_present("binary");
  336. let bytes = matches.opt_present("bytes");
  337. match (binary, bytes) {
  338. (true, true ) => Err(Misfire::Conflict("binary", "bytes")),
  339. (true, false) => Ok(SizeFormat::BinaryBytes),
  340. (false, true ) => Ok(SizeFormat::JustBytes),
  341. (false, false) => Ok(SizeFormat::DecimalBytes),
  342. }
  343. }
  344. }
  345. #[derive(PartialEq, Debug, Copy, Clone)]
  346. pub enum TimeType {
  347. FileAccessed,
  348. FileModified,
  349. FileCreated,
  350. }
  351. impl TimeType {
  352. pub fn header(&self) -> &'static str {
  353. match *self {
  354. TimeType::FileAccessed => "Date Accessed",
  355. TimeType::FileModified => "Date Modified",
  356. TimeType::FileCreated => "Date Created",
  357. }
  358. }
  359. }
  360. #[derive(PartialEq, Debug, Copy, Clone)]
  361. pub struct TimeTypes {
  362. accessed: bool,
  363. modified: bool,
  364. created: bool,
  365. }
  366. impl default::Default for TimeTypes {
  367. fn default() -> TimeTypes {
  368. TimeTypes { accessed: false, modified: true, created: false }
  369. }
  370. }
  371. impl TimeTypes {
  372. /// Find which field to use based on a user-supplied word.
  373. fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
  374. let possible_word = matches.opt_str("time");
  375. let modified = matches.opt_present("modified");
  376. let created = matches.opt_present("created");
  377. let accessed = matches.opt_present("accessed");
  378. if let Some(word) = possible_word {
  379. if modified {
  380. return Err(Misfire::Useless("modified", true, "time"));
  381. }
  382. else if created {
  383. return Err(Misfire::Useless("created", true, "time"));
  384. }
  385. else if accessed {
  386. return Err(Misfire::Useless("accessed", true, "time"));
  387. }
  388. match &word[..] {
  389. "mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
  390. "acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
  391. "cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
  392. field => Err(TimeTypes::none(field)),
  393. }
  394. }
  395. else {
  396. if modified || created || accessed {
  397. Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
  398. }
  399. else {
  400. Ok(TimeTypes::default())
  401. }
  402. }
  403. }
  404. /// How to display an error when the word didn't match with anything.
  405. fn none(field: &str) -> Misfire {
  406. Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field)))
  407. }
  408. }
  409. /// What to do when encountering a directory?
  410. #[derive(PartialEq, Debug, Copy, Clone)]
  411. pub enum DirAction {
  412. AsFile,
  413. List,
  414. Recurse(RecurseOptions),
  415. }
  416. impl DirAction {
  417. pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
  418. let recurse = matches.opt_present("recurse");
  419. let list = matches.opt_present("list-dirs");
  420. let tree = matches.opt_present("tree");
  421. match (recurse, list, tree) {
  422. (true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
  423. (_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
  424. (true, false, false) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
  425. (_ , _, true ) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
  426. (false, true, _ ) => Ok(DirAction::AsFile),
  427. (false, false, _ ) => Ok(DirAction::List),
  428. }
  429. }
  430. pub fn recurse_options(&self) -> Option<RecurseOptions> {
  431. match *self {
  432. DirAction::Recurse(opts) => Some(opts),
  433. _ => None,
  434. }
  435. }
  436. pub fn is_as_file(&self) -> bool {
  437. match *self {
  438. DirAction::AsFile => true,
  439. _ => false,
  440. }
  441. }
  442. pub fn is_tree(&self) -> bool {
  443. match *self {
  444. DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
  445. _ => false,
  446. }
  447. }
  448. }
  449. #[derive(PartialEq, Debug, Copy, Clone)]
  450. pub struct RecurseOptions {
  451. pub tree: bool,
  452. pub max_depth: Option<usize>,
  453. }
  454. impl RecurseOptions {
  455. pub fn deduce(matches: &getopts::Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
  456. let max_depth = if let Some(level) = matches.opt_str("level") {
  457. match level.parse() {
  458. Ok(l) => Some(l),
  459. Err(e) => return Err(Misfire::FailedParse(e)),
  460. }
  461. }
  462. else {
  463. None
  464. };
  465. Ok(RecurseOptions {
  466. tree: tree,
  467. max_depth: max_depth,
  468. })
  469. }
  470. pub fn is_too_deep(&self, depth: usize) -> bool {
  471. match self.max_depth {
  472. None => false,
  473. Some(d) => {
  474. d <= depth
  475. }
  476. }
  477. }
  478. }
  479. #[derive(PartialEq, Copy, Clone, Debug, Default)]
  480. pub struct Columns {
  481. size_format: SizeFormat,
  482. time_types: TimeTypes,
  483. inode: bool,
  484. links: bool,
  485. blocks: bool,
  486. group: bool,
  487. git: bool
  488. }
  489. impl Columns {
  490. pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
  491. Ok(Columns {
  492. size_format: try!(SizeFormat::deduce(matches)),
  493. time_types: try!(TimeTypes::deduce(matches)),
  494. inode: matches.opt_present("inode"),
  495. links: matches.opt_present("links"),
  496. blocks: matches.opt_present("blocks"),
  497. group: matches.opt_present("group"),
  498. git: cfg!(feature="git") && matches.opt_present("git"),
  499. })
  500. }
  501. pub fn should_scan_for_git(&self) -> bool {
  502. self.git
  503. }
  504. pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
  505. let mut columns = vec![];
  506. if self.inode {
  507. columns.push(Inode);
  508. }
  509. columns.push(Permissions);
  510. if self.links {
  511. columns.push(HardLinks);
  512. }
  513. columns.push(FileSize(self.size_format));
  514. if self.blocks {
  515. columns.push(Blocks);
  516. }
  517. columns.push(User);
  518. if self.group {
  519. columns.push(Group);
  520. }
  521. if self.time_types.modified {
  522. columns.push(Timestamp(TimeType::FileModified));
  523. }
  524. if self.time_types.created {
  525. columns.push(Timestamp(TimeType::FileCreated));
  526. }
  527. if self.time_types.accessed {
  528. columns.push(Timestamp(TimeType::FileAccessed));
  529. }
  530. if cfg!(feature="git") {
  531. if let Some(d) = dir {
  532. if self.should_scan_for_git() && d.has_git_repo() {
  533. columns.push(GitStatus);
  534. }
  535. }
  536. }
  537. columns
  538. }
  539. }
  540. #[cfg(test)]
  541. mod test {
  542. use super::Options;
  543. use super::Misfire;
  544. use feature::xattr;
  545. fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
  546. match misfire {
  547. Err(Misfire::Help(_)) => true,
  548. _ => false,
  549. }
  550. }
  551. #[test]
  552. fn help() {
  553. let opts = Options::getopts(&[ "--help".to_string() ]);
  554. assert!(is_helpful(opts))
  555. }
  556. #[test]
  557. fn help_with_file() {
  558. let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
  559. assert!(is_helpful(opts))
  560. }
  561. #[test]
  562. fn files() {
  563. let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
  564. assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
  565. }
  566. #[test]
  567. fn no_args() {
  568. let args = Options::getopts(&[]).unwrap().1;
  569. assert_eq!(args, vec![ ".".to_string() ])
  570. }
  571. #[test]
  572. fn file_sizes() {
  573. let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
  574. assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
  575. }
  576. #[test]
  577. fn just_binary() {
  578. let opts = Options::getopts(&[ "--binary".to_string() ]);
  579. assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
  580. }
  581. #[test]
  582. fn just_bytes() {
  583. let opts = Options::getopts(&[ "--bytes".to_string() ]);
  584. assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
  585. }
  586. #[test]
  587. fn long_across() {
  588. let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
  589. assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
  590. }
  591. #[test]
  592. fn oneline_across() {
  593. let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
  594. assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
  595. }
  596. #[test]
  597. fn just_header() {
  598. let opts = Options::getopts(&[ "--header".to_string() ]);
  599. assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
  600. }
  601. #[test]
  602. fn just_group() {
  603. let opts = Options::getopts(&[ "--group".to_string() ]);
  604. assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
  605. }
  606. #[test]
  607. fn just_inode() {
  608. let opts = Options::getopts(&[ "--inode".to_string() ]);
  609. assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
  610. }
  611. #[test]
  612. fn just_links() {
  613. let opts = Options::getopts(&[ "--links".to_string() ]);
  614. assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
  615. }
  616. #[test]
  617. fn just_blocks() {
  618. let opts = Options::getopts(&[ "--blocks".to_string() ]);
  619. assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
  620. }
  621. #[test]
  622. #[cfg(feature="git")]
  623. fn just_git() {
  624. let opts = Options::getopts(&[ "--git".to_string() ]);
  625. assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
  626. }
  627. #[test]
  628. fn extended_without_long() {
  629. if xattr::ENABLED {
  630. let opts = Options::getopts(&[ "--extended".to_string() ]);
  631. assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
  632. }
  633. }
  634. #[test]
  635. fn level_without_recurse_or_tree() {
  636. let opts = Options::getopts(&[ "--level".to_string(), "69105".to_string() ]);
  637. assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
  638. }
  639. }