options.rs 24 KB

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