Advertisement
Zv0n

Options edit

Mar 6th, 2016
136
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Rust 30.24 KB | None | 0 0
  1. use std::cmp;
  2. use std::env::var_os;
  3. use std::fmt;
  4. use std::num::ParseIntError;
  5. use std::os::unix::fs::MetadataExt;
  6. use std::ascii::AsciiExt;
  7.  
  8. use getopts;
  9. use natord;
  10.  
  11. use feature::xattr;
  12. use file::File;
  13. use output::{Grid, Details, GridDetails, Lines};
  14. use output::Colours;
  15. use output::column::{Columns, TimeTypes, SizeFormat};
  16. use term::dimensions;
  17.  
  18.  
  19. /// These **options** represent a parsed, error-checked versions of the
  20. /// user's command-line options.
  21. #[derive(PartialEq, Debug, Copy, Clone)]
  22. pub struct Options {
  23.  
  24.     /// The action to perform when encountering a directory rather than a
  25.     /// regular file.
  26.     pub dir_action: DirAction,
  27.  
  28.     /// How to sort and filter files before outputting them.
  29.     pub filter: FileFilter,
  30.  
  31.     /// The type of output to use (lines, grid, or details).
  32.     pub view: View,
  33. }
  34.  
  35. impl Options {
  36.  
  37.     /// Call getopts on the given slice of command-line strings.
  38.     #[allow(unused_results)]
  39.     pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
  40.         let mut opts = getopts::Options::new();
  41.  
  42.         opts.optflag("v", "version",   "display version of exa");
  43.         opts.optflag("?", "help",      "show list of command-line options");
  44.  
  45.         // Display options
  46.         opts.optflag("1", "oneline",   "display one entry per line");
  47.         opts.optflag("G", "grid",      "display entries in a grid view (default)");
  48.         opts.optflag("l", "long",      "display extended details and attributes");
  49.         opts.optflag("R", "recurse",   "recurse into directories");
  50.         opts.optflag("T", "tree",      "recurse into subdirectories in a tree view");
  51.         opts.optflag("x", "across",    "sort multi-column view entries across");
  52.         opts.optopt ("",  "color",     "when to show anything in colours", "WHEN");
  53.         opts.optopt ("",  "colour",    "when to show anything in colours (alternate spelling)", "WHEN");
  54.  
  55.         // Filtering and sorting options
  56.         opts.optflag("",  "group-directories-first", "list directories before other files");
  57.         opts.optflag("a", "all",       "show dot-files");
  58.         opts.optflag("d", "list-dirs", "list directories as regular files");
  59.         opts.optflag("r", "reverse",   "reverse order of files");
  60.         opts.optopt ("s", "sort",      "field to sort by", "WORD");
  61.  
  62.         // Long view options
  63.         opts.optflag("b", "binary",    "use binary prefixes in file sizes");
  64.         opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
  65.         opts.optflag("g", "group",     "show group as well as user");
  66.         opts.optflag("h", "header",    "show a header row at the top");
  67.         opts.optflag("H", "links",     "show number of hard links");
  68.         opts.optflag("i", "inode",     "show each file's inode number");
  69.         opts.optopt ("L", "level",     "maximum depth of recursion", "DEPTH");
  70.         opts.optflag("m", "modified",  "display timestamp of most recent modification");
  71.         opts.optflag("S", "blocks",    "show number of file system blocks");
  72.         opts.optopt ("t", "time",      "which timestamp to show for a file", "WORD");
  73.         opts.optflag("u", "accessed",  "display timestamp of last access for a file");
  74.         opts.optflag("U", "created",   "display timestamp of creation for a file");
  75.  
  76.         if cfg!(feature="git") {
  77.             opts.optflag("", "git", "show git status");
  78.         }
  79.  
  80.         if xattr::ENABLED {
  81.             opts.optflag("@", "extended", "display extended attribute keys and sizes");
  82.         }
  83.  
  84.         let matches = match opts.parse(args) {
  85.             Ok(m)   => m,
  86.             Err(e)  => return Err(Misfire::InvalidOptions(e)),
  87.         };
  88.  
  89.         if matches.opt_present("help") {
  90.             let mut help_string = "Usage:\n  exa [options] [files...]\n".to_owned();
  91.  
  92.             if !matches.opt_present("long") {
  93.                 help_string.push_str(OPTIONS);
  94.             }
  95.  
  96.             help_string.push_str(LONG_OPTIONS);
  97.  
  98.             if cfg!(feature="git") {
  99.                 help_string.push_str(GIT_HELP);
  100.                 help_string.push('\n');
  101.             }
  102.  
  103.             if xattr::ENABLED {
  104.                 help_string.push_str(EXTENDED_HELP);
  105.                 help_string.push('\n');
  106.             }
  107.  
  108.             return Err(Misfire::Help(help_string));
  109.         }
  110.         else if matches.opt_present("version") {
  111.             return Err(Misfire::Version);
  112.         }
  113.  
  114.         let options = try!(Options::deduce(&matches));
  115.         Ok((options, matches.free))
  116.     }
  117.  
  118.     /// Whether the View specified in this set of options includes a Git
  119.     /// status column. It's only worth trying to discover a repository if the
  120.     /// results will end up being displayed.
  121.     pub fn should_scan_for_git(&self) -> bool {
  122.         match self.view {
  123.             View::Details(Details { columns: Some(cols), .. }) => cols.should_scan_for_git(),
  124.             View::GridDetails(GridDetails { details: Details { columns: Some(cols), .. }, .. }) => cols.should_scan_for_git(),
  125.             _ => false,
  126.         }
  127.     }
  128. }
  129.  
  130. impl OptionSet for Options {
  131.     fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
  132.         let dir_action = try!(DirAction::deduce(&matches));
  133.         let filter = try!(FileFilter::deduce(&matches));
  134.         let view = try!(View::deduce(&matches, filter, dir_action));
  135.  
  136.         Ok(Options {
  137.             dir_action: dir_action,
  138.             view:       view,
  139.             filter:     filter,
  140.         })
  141.     }
  142. }
  143.  
  144.  
  145. #[derive(PartialEq, Debug, Copy, Clone)]
  146. pub enum View {
  147.     Details(Details),
  148.     Grid(Grid),
  149.     GridDetails(GridDetails),
  150.     Lines(Lines),
  151. }
  152.  
  153. impl View {
  154.     fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
  155.         use self::Misfire::*;
  156.  
  157.         let long = || {
  158.             if matches.opt_present("across") && !matches.opt_present("grid") {
  159.                 Err(Useless("across", true, "long"))
  160.             }
  161.             else if matches.opt_present("oneline") {
  162.                 Err(Useless("oneline", true, "long"))
  163.             }
  164.             else {
  165.                 let term_colours = try!(TerminalColours::deduce(matches));
  166.                 let colours = match term_colours {
  167.                     TerminalColours::Always    => Colours::colourful(),
  168.                     TerminalColours::Never     => Colours::plain(),
  169.                     TerminalColours::Automatic => {
  170.                         if dimensions().is_some() {
  171.                             Colours::colourful()
  172.                         }
  173.                         else {
  174.                             Colours::plain()
  175.                         }
  176.                     },
  177.                 };
  178.  
  179.                 let details = Details {
  180.                     columns: Some(try!(Columns::deduce(matches))),
  181.                     header: matches.opt_present("header"),
  182.                     recurse: dir_action.recurse_options(),
  183.                     filter: filter,
  184.                     xattr: xattr::ENABLED && matches.opt_present("extended"),
  185.                     colours: colours,
  186.                 };
  187.  
  188.                 Ok(details)
  189.             }
  190.         };
  191.  
  192.         let long_options_scan = || {
  193.             for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
  194.                 if matches.opt_present(option) {
  195.                     return Err(Useless(option, false, "long"));
  196.                 }
  197.             }
  198.  
  199.             if cfg!(feature="git") && matches.opt_present("git") {
  200.                 Err(Useless("git", false, "long"))
  201.             }
  202.             else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
  203.                 Err(Useless2("level", "recurse", "tree"))
  204.             }
  205.             else if xattr::ENABLED && matches.opt_present("extended") {
  206.                 Err(Useless("extended", false, "long"))
  207.             }
  208.             else {
  209.                 Ok(())
  210.             }
  211.         };
  212.  
  213.         let other_options_scan = || {
  214.             let term_colours = try!(TerminalColours::deduce(matches));
  215.             let term_width   = try!(TerminalWidth::deduce(matches));
  216.  
  217.             if let TerminalWidth::Set(width) = term_width {
  218.                 let colours = match term_colours {
  219.                     TerminalColours::Always    => Colours::colourful(),
  220.                     TerminalColours::Never     => Colours::plain(),
  221.                     TerminalColours::Automatic => Colours::colourful(),
  222.                 };
  223.  
  224.                 if matches.opt_present("oneline") {
  225.                     if matches.opt_present("across") {
  226.                         Err(Useless("across", true, "oneline"))
  227.                     }
  228.                     else {
  229.                         let lines = Lines {
  230.                              colours: colours,
  231.                         };
  232.  
  233.                         Ok(View::Lines(lines))
  234.                     }
  235.                 }
  236.                 else if matches.opt_present("tree") {
  237.                     let details = Details {
  238.                         columns: None,
  239.                         header: false,
  240.                         recurse: dir_action.recurse_options(),
  241.                         filter: filter,
  242.                         xattr: false,
  243.                         colours: colours,
  244.                     };
  245.  
  246.                     Ok(View::Details(details))
  247.                 }
  248.                 else {
  249.                     let grid = Grid {
  250.                         across: matches.opt_present("across"),
  251.                         console_width: width,
  252.                         colours: colours,
  253.                     };
  254.  
  255.                     Ok(View::Grid(grid))
  256.                 }
  257.             }
  258.             else {
  259.                 // If the terminal width couldn’t be matched for some reason, such
  260.                 // as the program’s stdout being connected to a file, then
  261.                 // fallback to the lines view.
  262.  
  263.                 let colours = match term_colours {
  264.                     TerminalColours::Always    => Colours::colourful(),
  265.                     TerminalColours::Never     => Colours::plain(),
  266.                     TerminalColours::Automatic => Colours::plain(),
  267.                 };
  268.  
  269.                 if matches.opt_present("tree") {
  270.                     let details = Details {
  271.                         columns: None,
  272.                         header: false,
  273.                         recurse: dir_action.recurse_options(),
  274.                         filter: filter,
  275.                         xattr: false,
  276.                         colours: colours,
  277.                     };
  278.  
  279.                     Ok(View::Details(details))
  280.                 }
  281.                 else {
  282.                     let lines = Lines {
  283.                          colours: colours,
  284.                     };
  285.  
  286.                     Ok(View::Lines(lines))
  287.                 }
  288.             }
  289.         };
  290.  
  291.         if matches.opt_present("long") {
  292.             let long_options = try!(long());
  293.  
  294.             if matches.opt_present("grid") {
  295.                 match other_options_scan() {
  296.                     Ok(View::Grid(grid)) => return Ok(View::GridDetails(GridDetails { grid: grid, details: long_options })),
  297.                     Ok(lines)            => return Ok(lines),
  298.                     Err(e)               => return Err(e),
  299.                 };
  300.             }
  301.             else {
  302.                 return Ok(View::Details(long_options));
  303.             }
  304.         }
  305.  
  306.         try!(long_options_scan());
  307.  
  308.         other_options_scan()
  309.     }
  310. }
  311.  
  312.  
  313. trait OptionSet: Sized {
  314.     fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire>;
  315. }
  316.  
  317.  
  318. /// The **file filter** processes a vector of files before outputting them,
  319. /// filtering and sorting the files depending on the user’s command-line
  320. /// flags.
  321. #[derive(Default, PartialEq, Debug, Copy, Clone)]
  322. pub struct FileFilter {
  323.     list_dirs_first: bool,
  324.     reverse: bool,
  325.     show_invisibles: bool,
  326.     sort_field: SortField,
  327. }
  328.  
  329. impl OptionSet for FileFilter {
  330.     fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
  331.         let sort_field = try!(SortField::deduce(&matches));
  332.  
  333.         Ok(FileFilter {
  334.             list_dirs_first: matches.opt_present("group-directories-first"),
  335.             reverse:         matches.opt_present("reverse"),
  336.             show_invisibles: matches.opt_present("all"),
  337.             sort_field:      sort_field,
  338.         })
  339.     }
  340. }
  341.  
  342. impl FileFilter {
  343.  
  344.     /// Remove every file in the given vector that does *not* pass the
  345.     /// filter predicate.
  346.     pub fn filter_files(&self, files: &mut Vec<File>) {
  347.         if !self.show_invisibles {
  348.             files.retain(|f| !f.is_dotfile());
  349.         }
  350.     }
  351.  
  352.     /// Sort the files in the given vector based on the sort field option.
  353.     pub fn sort_files(&self, files: &mut Vec<File>) {
  354.         files.sort_by(|a, b| self.compare_files(a, b));
  355.  
  356.         if self.reverse {
  357.             files.reverse();
  358.         }
  359.  
  360.         if self.list_dirs_first {
  361.             // This relies on the fact that `sort_by` is stable.
  362.             files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
  363.         }
  364.     }
  365.  
  366.     pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
  367.         match self.sort_field {
  368.             SortField::Unsorted      => cmp::Ordering::Equal,
  369.             SortField::Name          => natord::compare(&*a.name, &*b.name),
  370.             SortField::Name_nocaps   => natord::compare(&*a.name.to_ascii_lowercase(), &*b.name.to_ascii_lowercase()),
  371.             SortField::Size          => a.metadata.len().cmp(&b.metadata.len()),
  372.             SortField::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
  373.             SortField::ModifiedDate  => a.metadata.mtime().cmp(&b.metadata.mtime()),
  374.             SortField::AccessedDate  => a.metadata.atime().cmp(&b.metadata.atime()),
  375.             SortField::CreatedDate   => a.metadata.ctime().cmp(&b.metadata.ctime()),
  376.             SortField::Extension     => match a.ext.cmp(&b.ext) {
  377.                 cmp::Ordering::Equal  => natord::compare(&*a.name, &*b.name),
  378.                 order                 => order,
  379.             },
  380.         }
  381.     }
  382. }
  383.  
  384.  
  385. /// What to do when encountering a directory?
  386. #[derive(PartialEq, Debug, Copy, Clone)]
  387. pub enum DirAction {
  388.     AsFile,
  389.     List,
  390.     Recurse(RecurseOptions),
  391. }
  392.  
  393. impl DirAction {
  394.     pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
  395.         let recurse = matches.opt_present("recurse");
  396.         let list    = matches.opt_present("list-dirs");
  397.         let tree    = matches.opt_present("tree");
  398.  
  399.         match (recurse, list, tree) {
  400.             (true,  true,  _    )  => Err(Misfire::Conflict("recurse", "list-dirs")),
  401.             (_,     true,  true )  => Err(Misfire::Conflict("tree", "list-dirs")),
  402.             (true,  false, false)  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
  403.             (_   ,  _,     true )  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
  404.             (false, true,  _    )  => Ok(DirAction::AsFile),
  405.             (false, false, _    )  => Ok(DirAction::List),
  406.         }
  407.     }
  408.  
  409.     pub fn recurse_options(&self) -> Option<RecurseOptions> {
  410.         match *self {
  411.             DirAction::Recurse(opts) => Some(opts),
  412.             _ => None,
  413.         }
  414.     }
  415.  
  416.     pub fn treat_dirs_as_files(&self) -> bool {
  417.         match *self {
  418.             DirAction::AsFile => true,
  419.             DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
  420.             _ => false,
  421.         }
  422.     }
  423. }
  424.  
  425.  
  426. #[derive(PartialEq, Debug, Copy, Clone)]
  427. pub struct RecurseOptions {
  428.     pub tree:      bool,
  429.     pub max_depth: Option<usize>,
  430. }
  431.  
  432. impl RecurseOptions {
  433.     pub fn deduce(matches: &getopts::Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
  434.         let max_depth = if let Some(level) = matches.opt_str("level") {
  435.             match level.parse() {
  436.                 Ok(l)  => Some(l),
  437.                 Err(e) => return Err(Misfire::FailedParse(e)),
  438.             }
  439.         }
  440.         else {
  441.             None
  442.         };
  443.  
  444.         Ok(RecurseOptions {
  445.             tree: tree,
  446.             max_depth: max_depth,
  447.         })
  448.     }
  449.  
  450.     pub fn is_too_deep(&self, depth: usize) -> bool {
  451.         match self.max_depth {
  452.             None    => false,
  453.             Some(d) => {
  454.                 d <= depth
  455.             }
  456.         }
  457.     }
  458. }
  459.  
  460.  
  461. /// User-supplied field to sort by.
  462. #[derive(PartialEq, Debug, Copy, Clone)]
  463. pub enum SortField {
  464.     Unsorted, Name, Name_nocaps, Extension, Size, FileInode,
  465.     ModifiedDate, AccessedDate, CreatedDate,
  466. }
  467.  
  468. impl Default for SortField {
  469.     fn default() -> SortField {
  470.         SortField::Name
  471.     }
  472. }
  473.  
  474. impl OptionSet for SortField {
  475.     fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
  476.         if let Some(word) = matches.opt_str("sort") {
  477.             match &*word {
  478.                 "name" | "filename"   => Ok(SortField::Name),
  479.                 "nocaps" | "name_nocaps" => Ok(SortField::Name_nocaps),
  480.                 "size" | "filesize"   => Ok(SortField::Size),
  481.                 "ext"  | "extension"  => Ok(SortField::Extension),
  482.                 "mod"  | "modified"   => Ok(SortField::ModifiedDate),
  483.                 "acc"  | "accessed"   => Ok(SortField::AccessedDate),
  484.                 "cr"   | "created"    => Ok(SortField::CreatedDate),
  485.                 "none"                => Ok(SortField::Unsorted),
  486.                 "inode"               => Ok(SortField::FileInode),
  487.                 field                 => Err(Misfire::bad_argument("sort", field))
  488.             }
  489.         }
  490.         else {
  491.             Ok(SortField::default())
  492.         }
  493.     }
  494. }
  495.  
  496.  
  497. #[derive(PartialEq, Debug)]
  498. enum TerminalWidth {
  499.     Set(usize),
  500.     Unset,
  501. }
  502.  
  503. impl OptionSet for TerminalWidth {
  504.     fn deduce(_: &getopts::Matches) -> Result<TerminalWidth, Misfire> {
  505.         if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
  506.             match columns.parse() {
  507.                 Ok(width)  => Ok(TerminalWidth::Set(width)),
  508.                 Err(e)     => Err(Misfire::FailedParse(e)),
  509.             }
  510.         }
  511.         else if let Some((width, _)) = dimensions() {
  512.             Ok(TerminalWidth::Set(width))
  513.         }
  514.         else {
  515.             Ok(TerminalWidth::Unset)
  516.         }
  517.     }
  518. }
  519.  
  520.  
  521. #[derive(PartialEq, Debug)]
  522. enum TerminalColours {
  523.     Always,
  524.     Automatic,
  525.     Never,
  526. }
  527.  
  528. impl Default for TerminalColours {
  529.     fn default() -> TerminalColours {
  530.         TerminalColours::Automatic
  531.     }
  532. }
  533.  
  534. impl OptionSet for TerminalColours {
  535.     fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
  536.         if let Some(word) = matches.opt_str("color").or(matches.opt_str("colour")) {
  537.             match &*word {
  538.                 "always"              => Ok(TerminalColours::Always),
  539.                 "auto" | "automatic"  => Ok(TerminalColours::Automatic),
  540.                 "never"               => Ok(TerminalColours::Never),
  541.                 otherwise             => Err(Misfire::bad_argument("color", otherwise))
  542.             }
  543.         }
  544.         else {
  545.             Ok(TerminalColours::default())
  546.         }
  547.     }
  548. }
  549.  
  550.  
  551. impl OptionSet for Columns {
  552.     fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
  553.         Ok(Columns {
  554.             size_format: try!(SizeFormat::deduce(matches)),
  555.             time_types:  try!(TimeTypes::deduce(matches)),
  556.             inode:  matches.opt_present("inode"),
  557.             links:  matches.opt_present("links"),
  558.             blocks: matches.opt_present("blocks"),
  559.             group:  matches.opt_present("group"),
  560.             git:    cfg!(feature="git") && matches.opt_present("git"),
  561.         })
  562.     }
  563. }
  564.  
  565.  
  566. impl OptionSet for SizeFormat {
  567.  
  568.     /// Determine which file size to use in the file size column based on
  569.     /// the user’s options.
  570.     ///
  571.     /// The default mode is to use the decimal prefixes, as they are the
  572.     /// most commonly-understood, and don’t involve trying to parse large
  573.     /// strings of digits in your head. Changing the format to anything else
  574.     /// involves the `--binary` or `--bytes` flags, and these conflict with
  575.     /// each other.
  576.     fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
  577.         let binary = matches.opt_present("binary");
  578.         let bytes  = matches.opt_present("bytes");
  579.  
  580.         match (binary, bytes) {
  581.             (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
  582.             (true,  false)  => Ok(SizeFormat::BinaryBytes),
  583.             (false, true )  => Ok(SizeFormat::JustBytes),
  584.             (false, false)  => Ok(SizeFormat::DecimalBytes),
  585.         }
  586.     }
  587. }
  588.  
  589.  
  590. impl OptionSet for TimeTypes {
  591.  
  592.     /// Determine which of a file’s time fields should be displayed for it
  593.     /// based on the user’s options.
  594.     ///
  595.     /// There are two separate ways to pick which fields to show: with a
  596.     /// flag (such as `--modified`) or with a parameter (such as
  597.     /// `--time=modified`). An error is signaled if both ways are used.
  598.     ///
  599.     /// It’s valid to show more than one column by passing in more than one
  600.     /// option, but passing *no* options means that the user just wants to
  601.     /// see the default set.
  602.     fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
  603.         let possible_word = matches.opt_str("time");
  604.         let modified = matches.opt_present("modified");
  605.         let created  = matches.opt_present("created");
  606.         let accessed = matches.opt_present("accessed");
  607.  
  608.         if let Some(word) = possible_word {
  609.             if modified {
  610.                 return Err(Misfire::Useless("modified", true, "time"));
  611.             }
  612.             else if created {
  613.                 return Err(Misfire::Useless("created", true, "time"));
  614.             }
  615.             else if accessed {
  616.                 return Err(Misfire::Useless("accessed", true, "time"));
  617.             }
  618.  
  619.             match &*word {
  620.                 "mod" | "modified"  => Ok(TimeTypes { accessed: false, modified: true,  created: false }),
  621.                 "acc" | "accessed"  => Ok(TimeTypes { accessed: true,  modified: false, created: false }),
  622.                 "cr"  | "created"   => Ok(TimeTypes { accessed: false, modified: false, created: true  }),
  623.                 otherwise           => Err(Misfire::bad_argument("time", otherwise)),
  624.             }
  625.         }
  626.         else if modified || created || accessed {
  627.             Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
  628.         }
  629.         else {
  630.             Ok(TimeTypes::default())
  631.         }
  632.     }
  633. }
  634.  
  635.  
  636. /// One of these things could happen instead of listing files.
  637. #[derive(PartialEq, Debug)]
  638. pub enum Misfire {
  639.  
  640.     /// The getopts crate didn't like these arguments.
  641.     InvalidOptions(getopts::Fail),
  642.  
  643.     /// The user asked for help. This isn't strictly an error, which is why
  644.     /// this enum isn't named Error!
  645.     Help(String),
  646.  
  647.     /// The user wanted the version number.
  648.     Version,
  649.  
  650.     /// Two options were given that conflict with one another.
  651.     Conflict(&'static str, &'static str),
  652.  
  653.     /// An option was given that does nothing when another one either is or
  654.     /// isn't present.
  655.     Useless(&'static str, bool, &'static str),
  656.  
  657.     /// An option was given that does nothing when either of two other options
  658.     /// are not present.
  659.     Useless2(&'static str, &'static str, &'static str),
  660.  
  661.    /// A numeric option was given that failed to be parsed as a number.
  662.    FailedParse(ParseIntError),
  663. }
  664.  
  665. impl Misfire {
  666.  
  667.    /// The OS return code this misfire should signify.
  668.    pub fn error_code(&self) -> i32 {
  669.        if let Misfire::Help(_) = *self { 2 }
  670.                                   else { 3 }
  671.    }
  672.  
  673.    /// The Misfire that happens when an option gets given the wrong
  674.    /// argument. This has to use one of the `getopts` failure
  675.    /// variants--it’s meant to take just an option name, rather than an
  676.    /// option *and* an argument, but it works just as well.
  677.    pub fn bad_argument(option: &str, otherwise: &str) -> Misfire {
  678.        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--{} {}", option, otherwise)))
  679.    }
  680. }
  681.  
  682. impl fmt::Display for Misfire {
  683.    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  684.        use self::Misfire::*;
  685.  
  686.        match *self {
  687.            InvalidOptions(ref e)  => write!(f, "{}", e),
  688.            Help(ref text)         => write!(f, "{}", text),
  689.            Version                => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
  690.            Conflict(a, b)         => write!(f, "Option --{} conflicts with option {}.", a, b),
  691.            Useless(a, false, b)   => write!(f, "Option --{} is useless without option --{}.", a, b),
  692.            Useless(a, true, b)    => write!(f, "Option --{} is useless given option --{}.", a, b),
  693.            Useless2(a, b1, b2)    => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
  694.            FailedParse(ref e)     => write!(f, "Failed to parse number: {}", e),
  695.        }
  696.    }
  697. }
  698.  
  699. static OPTIONS: &'static str = r##"
  700. DISPLAY OPTIONS
  701.  -1, --oneline      display one entry per line
  702.  -G, --grid         display entries in a grid view (default)
  703.  -l, --long         display extended details and attributes
  704.  -R, --recurse      recurse into directories
  705.  -T, --tree         recurse into subdirectories in a tree view
  706.  -x, --across       sort multi-column view entries across
  707.  --color, --colour  when to colourise the output
  708.  
  709. FILTERING AND SORTING OPTIONS
  710.  -a, --all                  show dot-files
  711.  -d, --list-dirs            list directories as regular files
  712.  -r, --reverse              reverse order of files
  713.  -s, --sort WORD            field to sort by
  714.  --group-directories-first  list directories before other files
  715. "##;
  716.  
  717. static LONG_OPTIONS: &'static str = r##"
  718. LONG VIEW OPTIONS
  719.  -b, --binary       use binary prefixes in file sizes
  720.  -B, --bytes        list file sizes in bytes, without prefixes
  721.  -g, --group        show group as well as user
  722.  -h, --header       show a header row at the top
  723.  -H, --links        show number of hard links
  724.  -i, --inode        show each file's inode number
  725.   -L, --level DEPTH  maximum depth of recursion
  726.   -m, --modified     display timestamp of most recent modification
  727.   -S, --blocks       show number of file system blocks
  728.   -t, --time WORD    which timestamp to show for a file
  729.   -u, --accessed     display timestamp of last access for a file
  730.   -U, --created      display timestamp of creation for a file
  731. "##;
  732.  
  733. static GIT_HELP:      &'static str = r##"  --git              show git status for files"##;
  734. static EXTENDED_HELP: &'static str = r##"  -@, --extended     display extended attribute keys and sizes"##;
  735.  
  736. #[cfg(test)]
  737. mod test {
  738.    use super::Options;
  739.    use super::Misfire;
  740.    use feature::xattr;
  741.  
  742.    fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
  743.        match misfire {
  744.            Err(Misfire::Help(_)) => true,
  745.            _                     => false,
  746.        }
  747.    }
  748.  
  749.    #[test]
  750.    fn help() {
  751.        let opts = Options::getopts(&[ "--help".to_string() ]);
  752.        assert!(is_helpful(opts))
  753.    }
  754.  
  755.    #[test]
  756.    fn help_with_file() {
  757.        let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
  758.        assert!(is_helpful(opts))
  759.    }
  760.  
  761.    #[test]
  762.    fn files() {
  763.        let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
  764.        assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
  765.    }
  766.  
  767.    #[test]
  768.    fn no_args() {
  769.        let args = Options::getopts(&[]).unwrap().1;
  770.        assert!(args.is_empty());  // Listing the `.` directory is done in main.rs
  771.    }
  772.  
  773.    #[test]
  774.    fn file_sizes() {
  775.        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
  776.        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
  777.    }
  778.  
  779.    #[test]
  780.    fn just_binary() {
  781.        let opts = Options::getopts(&[ "--binary".to_string() ]);
  782.        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
  783.    }
  784.  
  785.    #[test]
  786.    fn just_bytes() {
  787.        let opts = Options::getopts(&[ "--bytes".to_string() ]);
  788.        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
  789.    }
  790.  
  791.    #[test]
  792.    fn long_across() {
  793.        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
  794.        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
  795.    }
  796.  
  797.    #[test]
  798.    fn oneline_across() {
  799.        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
  800.        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
  801.    }
  802.  
  803.    #[test]
  804.    fn just_header() {
  805.        let opts = Options::getopts(&[ "--header".to_string() ]);
  806.        assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
  807.    }
  808.  
  809.    #[test]
  810.    fn just_group() {
  811.        let opts = Options::getopts(&[ "--group".to_string() ]);
  812.        assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
  813.    }
  814.  
  815.    #[test]
  816.    fn just_inode() {
  817.        let opts = Options::getopts(&[ "--inode".to_string() ]);
  818.        assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
  819.    }
  820.  
  821.    #[test]
  822.    fn just_links() {
  823.        let opts = Options::getopts(&[ "--links".to_string() ]);
  824.        assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
  825.    }
  826.  
  827.    #[test]
  828.    fn just_blocks() {
  829.        let opts = Options::getopts(&[ "--blocks".to_string() ]);
  830.        assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
  831.    }
  832.  
  833.    #[test]
  834.    #[cfg(feature="git")]
  835.    fn just_git() {
  836.        let opts = Options::getopts(&[ "--git".to_string() ]);
  837.        assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
  838.    }
  839.  
  840.    #[test]
  841.    fn extended_without_long() {
  842.        if xattr::ENABLED {
  843.            let opts = Options::getopts(&[ "--extended".to_string() ]);
  844.            assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
  845.        }
  846.    }
  847.  
  848.    #[test]
  849.    fn level_without_recurse_or_tree() {
  850.        let opts = Options::getopts(&[ "--level".to_string(), "69105".to_string() ]);
  851.        assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
  852.    }
  853. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement