1
0

options.rs 23 KB

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