Ver código fonte

Switch to the new options parser

This commit removes the dependency on the ‘getopts’ crate entirely, and re-writes all its uses to use the new options parser instead.

As expected there are casualties galore:

- We now need to collect the options into a vector at the start, so we can use references to them, knowing they’ll be stored *somewhere*.
- Because OsString isn’t Display, its Debug impl gets used instead. (This is hopefully temporary)
- Options that take values (such as ‘sort’ or ‘time-style’) now parse those values with ‘to_string_lossy’. The ‘lossy’ part means “I’m at a loss for what to do here”
- Error messages got a lot worse, but “--tree --all --all” is now a special case of error rather than just another Misfire::Useless.
- Some tests had to be re-written to deal with the fact that the parser works with references.
- ParseError loses its lifetime and owns its contents, to avoid having to attach <'a> to Misfire.
- The parser now takes an iterator instead of a slice.
- OsStrings can’t be ‘match’ patterns, so the code devolves to using long Eq chains instead.
- Make a change to the xtest that assumed an input argument with invalid UTF-8 in was always an error to stderr, when that now in fact works!
- Fix a bug in Vagrant where ‘exa’ and ‘rexa’ didn’t properly escape filenames with spaces in.
Benjamin Sago 8 anos atrás
pai
commit
2d1f462bfa
11 arquivos alterados com 429 adições e 307 exclusões
  1. 2 2
      Vagrantfile
  2. 3 2
      src/bin/main.rs
  3. 11 12
      src/exa.rs
  4. 11 12
      src/options/dir_action.rs
  5. 57 33
      src/options/filter.rs
  6. 64 0
      src/options/flags.rs
  7. 26 23
      src/options/misfire.rs
  8. 86 101
      src/options/mod.rs
  9. 68 42
      src/options/parser.rs
  10. 100 79
      src/options/view.rs
  11. 1 1
      xtests/run.sh

+ 2 - 2
Vagrantfile

@@ -59,8 +59,8 @@ Vagrant.configure(2) do |config|
     config.vm.provision :shell, privileged: true, inline: <<-EOF
         set -xe
 
-        echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \\$*" > /usr/bin/exa
-        echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \\$*" > /usr/bin/rexa
+        echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \"\\$*\"" > /usr/bin/exa
+        echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \"\\$*\"" > /usr/bin/rexa
         chmod +x /usr/bin/{exa,rexa}
     EOF
 

+ 3 - 2
src/bin/main.rs

@@ -1,14 +1,15 @@
 extern crate exa;
 use exa::Exa;
 
+use std::ffi::OsString;
 use std::env::args_os;
 use std::io::{stdout, stderr, Write, ErrorKind};
 use std::process::exit;
 
 
 fn main() {
-    let args = args_os().skip(1);
-    match Exa::new(args, &mut stdout()) {
+    let args: Vec<OsString> = args_os().skip(1).collect();
+    match Exa::new(args.iter(), &mut stdout()) {
         Ok(mut exa) => {
             match exa.run() {
                 Ok(exit_status) => exit(exit_status),

+ 11 - 12
src/exa.rs

@@ -3,7 +3,6 @@
 
 extern crate ansi_term;
 extern crate datetime;
-extern crate getopts;
 extern crate glob;
 extern crate libc;
 extern crate locale;
@@ -22,7 +21,7 @@ extern crate zoneinfo_compiled;
 extern crate lazy_static;
 
 
-use std::ffi::OsStr;
+use std::ffi::{OsStr, OsString};
 use std::io::{stderr, Write, Result as IOResult};
 use std::path::{Component, PathBuf};
 
@@ -41,7 +40,7 @@ mod term;
 
 
 /// The main program wrapper.
-pub struct Exa<'w, W: Write + 'w> {
+pub struct Exa<'args, 'w, W: Write + 'w> {
 
     /// List of command-line options, having been successfully parsed.
     pub options: Options,
@@ -53,12 +52,12 @@ pub struct Exa<'w, W: Write + 'w> {
 
     /// List of the free command-line arguments that should correspond to file
     /// names (anything that isn’t an option).
-    pub args: Vec<String>,
+    pub args: Vec<&'args OsStr>,
 }
 
-impl<'w, W: Write + 'w> Exa<'w, W> {
-    pub fn new<C>(args: C, writer: &'w mut W) -> Result<Exa<'w, W>, Misfire>
-    where C: IntoIterator, C::Item: AsRef<OsStr> {
+impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
+    pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
+    where I: Iterator<Item=&'args OsString> {
         Options::getopts(args).map(move |(options, args)| {
             Exa { options, writer, args }
         })
@@ -71,20 +70,20 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
 
         // List the current directory by default, like ls.
         if self.args.is_empty() {
-            self.args.push(".".to_owned());
+            self.args = vec![ OsStr::new(".") ];
         }
 
-        for file_name in &self.args {
-            match File::new(PathBuf::from(file_name), None, None) {
+        for file_path in &self.args {
+            match File::new(PathBuf::from(file_path), None, None) {
                 Err(e) => {
                     exit_status = 2;
-                    writeln!(stderr(), "{}: {}", file_name, e)?;
+                    writeln!(stderr(), "{:?}: {}", file_path, e)?;
                 },
                 Ok(f) => {
                     if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() {
                         match f.to_dir(self.options.should_scan_for_git()) {
                             Ok(d) => dirs.push(d),
-                            Err(e) => writeln!(stderr(), "{}: {}", file_name, e)?,
+                            Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?,
                         }
                     }
                     else {

+ 11 - 12
src/options/dir_action.rs

@@ -1,24 +1,23 @@
-use getopts;
+use options::parser::Matches;
+use options::{flags, Misfire};
 
-use options::misfire::Misfire;
 use fs::dir_action::{DirAction, RecurseOptions};
 
 
-
 impl DirAction {
 
     /// Determine which action to perform when trying to list a directory.
-    pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
-        let recurse = matches.opt_present("recurse");
-        let list    = matches.opt_present("list-dirs");
-        let tree    = matches.opt_present("tree");
+    pub fn deduce(matches: &Matches) -> Result<DirAction, Misfire> {
+        let recurse = matches.has(&flags::RECURSE);
+        let list    = matches.has(&flags::LIST_DIRS);
+        let tree    = matches.has(&flags::TREE);
 
         match (recurse, list, tree) {
 
             // You can't --list-dirs along with --recurse or --tree because
             // they already automatically list directories.
-            (true,  true,  _    )  => Err(Misfire::Conflict("recurse", "list-dirs")),
-            (_,     true,  true )  => Err(Misfire::Conflict("tree", "list-dirs")),
+            (true,  true,  _    )  => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)),
+            (_,     true,  true )  => Err(Misfire::Conflict(&flags::TREE,    &flags::LIST_DIRS)),
 
             (_   ,  _,     true )  => Ok(DirAction::Recurse(RecurseOptions::deduce(matches, true)?)),
             (true,  false, false)  => Ok(DirAction::Recurse(RecurseOptions::deduce(matches, false)?)),
@@ -32,9 +31,9 @@ impl DirAction {
 impl RecurseOptions {
 
     /// Determine which files should be recursed into.
-    pub fn deduce(matches: &getopts::Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
-        let max_depth = if let Some(level) = matches.opt_str("level") {
-            match level.parse() {
+    pub fn deduce(matches: &Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
+        let max_depth = if let Some(level) = matches.get(&flags::LEVEL) {
+            match level.to_string_lossy().parse() {
                 Ok(l)   => Some(l),
                 Err(e)  => return Err(Misfire::FailedParse(e)),
             }

+ 57 - 33
src/options/filter.rs

@@ -1,19 +1,20 @@
-use getopts;
 use glob;
 
 use fs::DotFilter;
 use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns};
-use options::misfire::Misfire;
+
+use options::{flags, Misfire};
+use options::parser::Matches;
 
 
 impl FileFilter {
 
     /// Determines the set of file filter options to use, based on the user’s
     /// command-line arguments.
-    pub fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
+    pub fn deduce(matches: &Matches) -> Result<FileFilter, Misfire> {
         Ok(FileFilter {
-            list_dirs_first: matches.opt_present("group-directories-first"),
-            reverse:         matches.opt_present("reverse"),
+            list_dirs_first: matches.has(&flags::DIRS_FIRST),
+            reverse:         matches.has(&flags::REVERSE),
             sort_field:      SortField::deduce(matches)?,
             dot_filter:      DotFilter::deduce(matches)?,
             ignore_patterns: IgnorePatterns::deduce(matches)?,
@@ -34,45 +35,67 @@ impl SortField {
     /// Determine the sort field to use, based on the presence of a “sort”
     /// argument. This will return `Err` if the option is there, but does not
     /// correspond to a valid field.
-    fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
+    fn deduce(matches: &Matches) -> Result<SortField, Misfire> {
 
         const SORTS: &[&str] = &[ "name", "Name", "size", "extension",
                                   "Extension", "modified", "accessed",
                                   "created", "inode", "type", "none" ];
 
-        if let Some(word) = matches.opt_str("sort") {
-            match &*word {
-                "name" | "filename"   => Ok(SortField::Name(SortCase::Sensitive)),
-                "Name" | "Filename"   => Ok(SortField::Name(SortCase::Insensitive)),
-                "size" | "filesize"   => Ok(SortField::Size),
-                "ext"  | "extension"  => Ok(SortField::Extension(SortCase::Sensitive)),
-                "Ext"  | "Extension"  => Ok(SortField::Extension(SortCase::Insensitive)),
-                "mod"  | "modified"   => Ok(SortField::ModifiedDate),
-                "acc"  | "accessed"   => Ok(SortField::AccessedDate),
-                "cr"   | "created"    => Ok(SortField::CreatedDate),
-                "inode"               => Ok(SortField::FileInode),
-                "type"                => Ok(SortField::FileType),
-                "none"                => Ok(SortField::Unsorted),
-                field                 => Err(Misfire::bad_argument("sort", field, SORTS))
-            }
+        let word = match matches.get(&flags::SORT) {
+            Some(w)  => w,
+            None     => return Ok(SortField::default()),
+        };
+
+        if word == "name" || word == "filename" {
+            Ok(SortField::Name(SortCase::Sensitive))
+        }
+        else if word == "Name" || word == "Filename" {
+            Ok(SortField::Name(SortCase::Insensitive))
+        }
+        else if word == "size" || word == "filesize" {
+            Ok(SortField::Size)
+        }
+        else if word == "ext" || word == "extension" {
+            Ok(SortField::Extension(SortCase::Sensitive))
+        }
+        else if word == "Ext" || word == "Extension" {
+            Ok(SortField::Extension(SortCase::Insensitive))
+        }
+        else if word == "mod" || word == "modified" {
+            Ok(SortField::ModifiedDate)
+        }
+        else if word == "acc" || word == "accessed" {
+            Ok(SortField::AccessedDate)
+        }
+        else if word == "cr" || word == "created" {
+            Ok(SortField::CreatedDate)
+        }
+        else if word == "inode" {
+            Ok(SortField::FileInode)
+        }
+        else if word == "type" {
+            Ok(SortField::FileType)
+        }
+        else if word == "none" {
+            Ok(SortField::Unsorted)
         }
         else {
-            Ok(SortField::default())
+            Err(Misfire::bad_argument(&flags::SORT, word, SORTS))
         }
     }
 }
 
 
 impl DotFilter {
-    pub fn deduce(matches: &getopts::Matches) -> Result<DotFilter, Misfire> {
-        let dots = match matches.opt_count("all") {
+    pub fn deduce(matches: &Matches) -> Result<DotFilter, Misfire> {
+        let dots = match matches.count(&flags::ALL) {
             0 => return Ok(DotFilter::JustFiles),
             1 => DotFilter::Dotfiles,
             _ => DotFilter::DotfilesAndDots,
         };
 
-        if matches.opt_present("tree") {
-            Err(Misfire::Useless("all --all", true, "tree"))
+        if matches.has(&flags::TREE) {
+            Err(Misfire::TreeAllAll)
         }
         else {
             Ok(dots)
@@ -85,14 +108,15 @@ impl IgnorePatterns {
 
     /// Determines the set of file filter options to use, based on the user’s
     /// command-line arguments.
-    pub fn deduce(matches: &getopts::Matches) -> Result<IgnorePatterns, Misfire> {
-        let patterns = match matches.opt_str("ignore-glob") {
+    pub fn deduce(matches: &Matches) -> Result<IgnorePatterns, Misfire> {
+        let patterns = match matches.get(&flags::IGNORE_GLOB) {
             None => Ok(Vec::new()),
-            Some(is) => is.split('|').map(|a| glob::Pattern::new(a)).collect(),
-        };
+            Some(is) => is.to_string_lossy().split('|').map(|a| glob::Pattern::new(a)).collect(),
+        }?;
 
-        Ok(IgnorePatterns {
-            patterns: patterns?,
-        })
+        // TODO: is to_string_lossy really the best way to handle
+        // invalid UTF-8 there?
+
+        Ok(IgnorePatterns { patterns })
     }
 }

+ 64 - 0
src/options/flags.rs

@@ -0,0 +1,64 @@
+use options::parser::{Arg, Args, TakesValue};
+
+
+// exa options
+pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version",  takes_value: TakesValue::Forbidden };
+pub static HELP:    Arg = Arg { short: Some(b'?'), long: "help",     takes_value: TakesValue::Forbidden };
+
+// display options
+pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline",  takes_value: TakesValue::Forbidden };
+pub static LONG:     Arg = Arg { short: Some(b'l'), long: "long",     takes_value: TakesValue::Forbidden };
+pub static GRID:     Arg = Arg { short: Some(b'G'), long: "grid",     takes_value: TakesValue::Forbidden };
+pub static ACROSS:   Arg = Arg { short: Some(b'x'), long: "across",   takes_value: TakesValue::Forbidden };
+pub static RECURSE:  Arg = Arg { short: Some(b'R'), long: "recurse",  takes_value: TakesValue::Forbidden };
+pub static TREE:     Arg = Arg { short: Some(b'T'), long: "tree",     takes_value: TakesValue::Forbidden };
+pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden };
+
+pub static COLOR:  Arg = Arg { short: None, long: "color",  takes_value: TakesValue::Necessary };
+pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary };
+
+pub static COLOR_SCALE:  Arg = Arg { short: None, long: "color-scale",  takes_value: TakesValue::Forbidden };
+pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden };
+
+// filtering and sorting options
+pub static ALL:         Arg = Arg { short: Some(b'a'), long: "all",         takes_value: TakesValue::Forbidden };
+pub static LIST_DIRS:   Arg = Arg { short: Some(b'd'), long: "list-dirs",   takes_value: TakesValue::Forbidden };
+pub static LEVEL:       Arg = Arg { short: Some(b'L'), long: "level",       takes_value: TakesValue::Forbidden };
+pub static REVERSE:     Arg = Arg { short: Some(b'r'), long: "reverse",     takes_value: TakesValue::Forbidden };
+pub static SORT:        Arg = Arg { short: Some(b's'), long: "sort",        takes_value: TakesValue::Necessary };
+pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary };
+pub static DIRS_FIRST:  Arg = Arg { short: None, long: "group-directories-first",  takes_value: TakesValue::Forbidden };
+
+// display options
+pub static BINARY:     Arg = Arg { short: Some(b'b'), long: "binary",     takes_value: TakesValue::Forbidden };
+pub static BYTES:      Arg = Arg { short: Some(b'B'), long: "bytes",      takes_value: TakesValue::Forbidden };
+pub static GROUP:      Arg = Arg { short: Some(b'g'), long: "group",      takes_value: TakesValue::Forbidden };
+pub static HEADER:     Arg = Arg { short: Some(b'h'), long: "header",     takes_value: TakesValue::Forbidden };
+pub static INODE:      Arg = Arg { short: Some(b'i'), long: "inode",      takes_value: TakesValue::Forbidden };
+pub static LINKS:      Arg = Arg { short: Some(b'H'), long: "links",      takes_value: TakesValue::Forbidden };
+pub static MODIFIED:   Arg = Arg { short: Some(b'm'), long: "modified",   takes_value: TakesValue::Forbidden };
+pub static BLOCKS:     Arg = Arg { short: Some(b'S'), long: "blocks",     takes_value: TakesValue::Forbidden };
+pub static TIME:       Arg = Arg { short: Some(b't'), long: "time",       takes_value: TakesValue::Forbidden };
+pub static ACCESSED:   Arg = Arg { short: Some(b'u'), long: "accessed",   takes_value: TakesValue::Forbidden };
+pub static CREATED:    Arg = Arg { short: Some(b'U'), long: "created",    takes_value: TakesValue::Forbidden };
+pub static TIME_STYLE: Arg = Arg { short: None,       long: "time-style", takes_value: TakesValue::Necessary };
+
+// optional feature options
+pub static GIT:       Arg = Arg { short: None,       long: "git",      takes_value: TakesValue::Forbidden };
+pub static EXTENDED:  Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden };
+
+
+pub static ALL_ARGS: Args = Args(&[
+    &VERSION, &HELP,
+
+    &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY,
+    &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE,
+
+    &ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &IGNORE_GLOB, &DIRS_FIRST,
+
+    &BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &BLOCKS,
+    &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
+
+    &GIT, &EXTENDED,
+]);
+

+ 26 - 23
src/options/misfire.rs

@@ -1,10 +1,11 @@
+use std::ffi::{OsStr, OsString};
 use std::fmt;
 use std::num::ParseIntError;
 
-use getopts;
 use glob;
 
 use options::help::HelpString;
+use options::parser::{Arg, ParseError};
 
 
 /// A list of legal choices for an argument-taking option
@@ -13,7 +14,7 @@ pub struct Choices(&'static [&'static str]);
 
 impl fmt::Display for Choices {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "(choices: {})", self.0.join(" "))
+        write!(f, "(choices: {})", self.0.join(", "))
     }
 }
 
@@ -22,11 +23,11 @@ impl fmt::Display for Choices {
 #[derive(PartialEq, Debug)]
 pub enum Misfire {
 
-    /// The getopts crate didn’t like these arguments.
-    InvalidOptions(getopts::Fail),
+    /// The getopts crate didn’t like these Arguments.
+    InvalidOptions(ParseError),
 
-    /// The user supplied an illegal choice to an argument
-    BadArgument(getopts::Fail, Choices),
+    /// The user supplied an illegal choice to an Argument.
+    BadArgument(&'static Arg, OsString, Choices),
 
     /// The user asked for help. This isn’t strictly an error, which is why
     /// this enum isn’t named Error!
@@ -36,15 +37,18 @@ pub enum Misfire {
     Version,
 
     /// Two options were given that conflict with one another.
-    Conflict(&'static str, &'static str),
+    Conflict(&'static Arg, &'static Arg),
 
     /// An option was given that does nothing when another one either is or
     /// isn't present.
-    Useless(&'static str, bool, &'static str),
+    Useless(&'static Arg, bool, &'static Arg),
 
     /// An option was given that does nothing when either of two other options
     /// are not present.
-    Useless2(&'static str, &'static str, &'static str),
+    Useless2(&'static Arg, &'static Arg, &'static Arg),
+
+    /// A very specific edge case where --tree can’t be used with --all twice.
+    TreeAllAll,
 
     /// A numeric option was given that failed to be parsed as a number.
     FailedParse(ParseIntError),
@@ -68,10 +72,8 @@ impl Misfire {
     /// argument. This has to use one of the `getopts` failure
     /// variants--it’s meant to take just an option name, rather than an
     /// option *and* an argument, but it works just as well.
-    pub fn bad_argument(option: &str, otherwise: &str, legal: &'static [&'static str]) -> Misfire {
-        Misfire::BadArgument(getopts::Fail::UnrecognizedOption(format!(
-            "--{} {}",
-            option, otherwise)), Choices(legal))
+    pub fn bad_argument(option: &'static Arg, otherwise: &OsStr, legal: &'static [&'static str]) -> Misfire {
+        Misfire::BadArgument(option, otherwise.to_os_string(), Choices(legal))
     }
 }
 
@@ -86,16 +88,17 @@ impl fmt::Display for Misfire {
         use self::Misfire::*;
 
         match *self {
-            InvalidOptions(ref e)      => write!(f, "{}", e),
-            BadArgument(ref e, ref c)  => write!(f, "{} {}", e, c),
-            Help(ref text)             => write!(f, "{}", text),
-            Version                    => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
-            Conflict(a, b)             => write!(f, "Option --{} conflicts with option {}.", a, b),
-            Useless(a, false, b)       => write!(f, "Option --{} is useless without option --{}.", a, b),
-            Useless(a, true, b)        => write!(f, "Option --{} is useless given option --{}.", a, b),
-            Useless2(a, b1, b2)        => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
-            FailedParse(ref e)         => write!(f, "Failed to parse number: {}", e),
-            FailedGlobPattern(ref e)   => write!(f, "Failed to parse glob pattern: {}", e),
+            BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no value {:?} (Choices: {})", a, b, c),
+            InvalidOptions(ref e)            => write!(f, "{:?}", e),
+            Help(ref text)                   => write!(f, "{}", text),
+            Version                          => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
+            Conflict(ref a, ref b)           => write!(f, "Option {} conflicts with option {}.", a, b),
+            Useless(ref a, false, ref b)     => write!(f, "Option {} is useless without option {}.", a, b),
+            Useless(ref a, true, ref b)      => write!(f, "Option {} is useless given option {}.", a, b),
+            Useless2(ref a, ref b1, ref b2)  => write!(f, "Option {} is useless without options {} or {}.", a, b1, b2),
+            TreeAllAll                       => write!(f, "Option --tree is useless given --all --all."),
+            FailedParse(ref e)               => write!(f, "Failed to parse number: {}", e),
+            FailedGlobPattern(ref e)         => write!(f, "Failed to parse glob pattern: {}", e),
         }
     }
 }

+ 86 - 101
src/options/mod.rs

@@ -69,9 +69,7 @@
 //! it’s clear what the user wants.
 
 
-use std::ffi::OsStr;
-
-use getopts;
+use std::ffi::{OsStr, OsString};
 
 use fs::feature::xattr;
 use fs::dir_action::DirAction;
@@ -90,6 +88,8 @@ mod misfire;
 pub use self::misfire::Misfire;
 
 mod parser;
+mod flags;
+use self::parser::Matches;
 
 
 /// These **options** represent a parsed, error-checked versions of the
@@ -118,77 +118,29 @@ impl Options {
 
     /// Call getopts on the given slice of command-line strings.
     #[allow(unused_results)]
-    pub fn getopts<C>(args: C) -> Result<(Options, Vec<String>), Misfire>
-    where C: IntoIterator, C::Item: AsRef<OsStr> {
-        let mut opts = getopts::Options::new();
-
-        opts.optflag("v", "version",   "show version of exa");
-        opts.optflag("?", "help",      "show list of command-line options");
-
-        // Display options
-        opts.optflag("1", "oneline",      "display one entry per line");
-        opts.optflag("l", "long",         "display extended file metadata in a table");
-        opts.optflag("G", "grid",         "display entries as a grid (default)");
-        opts.optflag("x", "across",       "sort the grid across, rather than downwards");
-        opts.optflag("R", "recurse",      "recurse into directories");
-        opts.optflag("T", "tree",         "recurse into directories as a tree");
-        opts.optflag("F", "classify",     "display type indicator by file names (one of */=@|)");
-        opts.optopt ("",  "color",        "when to use terminal colours", "WHEN");
-        opts.optopt ("",  "colour",       "when to use terminal colours", "WHEN");
-        opts.optflag("",  "color-scale",  "highlight levels of file sizes distinctly");
-        opts.optflag("",  "colour-scale", "highlight levels of file sizes distinctly");
-
-        // Filtering and sorting options
-        opts.optflag("",  "group-directories-first", "sort directories before other files");
-        opts.optflagmulti("a", "all",    "show hidden and 'dot' files");
-        opts.optflag("d", "list-dirs",   "list directories like regular files");
-        opts.optopt ("L", "level",       "limit the depth of recursion", "DEPTH");
-        opts.optflag("r", "reverse",     "reverse the sert order");
-        opts.optopt ("s", "sort",        "which field to sort by", "WORD");
-        opts.optopt ("I", "ignore-glob", "ignore files that match these glob patterns", "GLOB1|GLOB2...");
-
-        // Long view options
-        opts.optflag("b", "binary",     "list file sizes with binary prefixes");
-        opts.optflag("B", "bytes",      "list file sizes in bytes, without prefixes");
-        opts.optflag("g", "group",      "list each file's group");
-        opts.optflag("h", "header",     "add a header row to each column");
-        opts.optflag("H", "links",      "list each file's number of hard links");
-        opts.optflag("i", "inode",      "list each file's inode number");
-        opts.optflag("m", "modified",   "use the modified timestamp field");
-        opts.optflag("S", "blocks",     "list each file's number of file system blocks");
-        opts.optopt ("t", "time",       "which timestamp field to show", "WORD");
-        opts.optflag("u", "accessed",   "use the accessed timestamp field");
-        opts.optflag("U", "created",    "use the created timestamp field");
-        opts.optopt ("",  "time-style", "how to format timestamp fields", "STYLE");
-
-        if cfg!(feature="git") {
-            opts.optflag("", "git", "list each file's git status");
-        }
-
-        if xattr::ENABLED {
-            opts.optflag("@", "extended", "list each file's extended attribute keys and sizes");
-        }
+    pub fn getopts<'args, I>(args: I) -> Result<(Options, Vec<&'args OsStr>), Misfire>
+    where I: IntoIterator<Item=&'args OsString> {
 
-        let matches = match opts.parse(args) {
+        let matches = match parser::parse(&flags::ALL_ARGS, args) {
             Ok(m)   => m,
             Err(e)  => return Err(Misfire::InvalidOptions(e)),
         };
 
-        if matches.opt_present("help") {
+        if matches.has(&flags::HELP) {
             let help = HelpString {
-                only_long: matches.opt_present("long"),
+                only_long: matches.has(&flags::LONG),
                 git: cfg!(feature="git"),
                 xattrs: xattr::ENABLED,
             };
 
             return Err(Misfire::Help(help));
         }
-        else if matches.opt_present("version") {
+        else if matches.has(&flags::VERSION) {
             return Err(Misfire::Version);
         }
 
         let options = Options::deduce(&matches)?;
-        Ok((options, matches.free))
+        Ok((options, matches.frees))
     }
 
     /// Whether the View specified in this set of options includes a Git
@@ -204,7 +156,7 @@ impl Options {
 
     /// Determines the complete set of options based on the given command-line
     /// arguments, after they’ve been parsed.
-    fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
+    fn deduce(matches: &Matches) -> Result<Options, Misfire> {
         let dir_action = DirAction::deduce(matches)?;
         let filter = FileFilter::deduce(matches)?;
         let view = View::deduce(matches)?;
@@ -214,9 +166,11 @@ impl Options {
 }
 
 
+
 #[cfg(test)]
 mod test {
-    use super::{Options, Misfire};
+    use super::{Options, Misfire, flags};
+    use std::ffi::OsString;
     use fs::DotFilter;
     use fs::filter::{SortField, SortCase};
     use fs::feature::xattr;
@@ -228,152 +182,183 @@ mod test {
         }
     }
 
+    /// Creates an `OSStr` (used in tests)
+    #[cfg(test)]
+    fn os(input: &'static str) -> OsString {
+        let mut os = OsString::new();
+        os.push(input);
+        os
+    }
+
+
     #[test]
     fn help() {
-        let opts = Options::getopts(&[ "--help".to_string() ]);
+        let args = [ os("--help") ];
+        let opts = Options::getopts(&args);
         assert!(is_helpful(opts))
     }
 
     #[test]
     fn help_with_file() {
-        let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
+        let args = [ os("--help"), os("me") ];
+        let opts = Options::getopts(&args);
         assert!(is_helpful(opts))
     }
 
     #[test]
     fn files() {
-        let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
-        assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
+        let args = [ os("this file"), os("that file") ];
+        let outs = Options::getopts(&args).unwrap().1;
+        assert_eq!(outs, vec![ &os("this file"), &os("that file") ])
     }
 
     #[test]
     fn no_args() {
-        let nothing: Vec<String> = Vec::new();
-        let args = Options::getopts(&nothing).unwrap().1;
-        assert!(args.is_empty());  // Listing the `.` directory is done in main.rs
+        let nothing: Vec<OsString> = Vec::new();
+        let outs = Options::getopts(&nothing).unwrap().1;
+        assert!(outs.is_empty());  // Listing the `.` directory is done in main.rs
     }
 
     #[test]
     fn file_sizes() {
-        let opts = Options::getopts(&[ "--long", "--binary", "--bytes" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
+        let args = [ os("--long"), os("--binary"), os("--bytes") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Conflict(&flags::BINARY, &flags::BYTES))
     }
 
     #[test]
     fn just_binary() {
-        let opts = Options::getopts(&[ "--binary" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
+        let args = [ os("--binary") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::BINARY, false, &flags::LONG))
     }
 
     #[test]
     fn just_bytes() {
-        let opts = Options::getopts(&[ "--bytes" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
+        let args = [ os("--bytes") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::BYTES, false, &flags::LONG))
     }
 
     #[test]
     fn long_across() {
-        let opts = Options::getopts(&[ "--long", "--across" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
+        let args = [ os("--long"), os("--across") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::LONG))
     }
 
     #[test]
     fn oneline_across() {
-        let opts = Options::getopts(&[ "--oneline", "--across" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
+        let args = [ os("--oneline"), os("--across") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
     }
 
     #[test]
     fn just_header() {
-        let opts = Options::getopts(&[ "--header" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
+        let args = [ os("--header") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::HEADER, false, &flags::LONG))
     }
 
     #[test]
     fn just_group() {
-        let opts = Options::getopts(&[ "--group" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
+        let args = [ os("--group") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::GROUP, false, &flags::LONG))
     }
 
     #[test]
     fn just_inode() {
-        let opts = Options::getopts(&[ "--inode" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
+        let args = [ os("--inode") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::INODE, false, &flags::LONG))
     }
 
     #[test]
     fn just_links() {
-        let opts = Options::getopts(&[ "--links" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
+        let args = [ os("--links") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::LINKS, false, &flags::LONG))
     }
 
     #[test]
     fn just_blocks() {
-        let opts = Options::getopts(&[ "--blocks" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
+        let args = [ os("--blocks") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::BLOCKS, false, &flags::LONG))
     }
 
     #[test]
     fn test_sort_size() {
-        let opts = Options::getopts(&[ "--sort=size" ]);
+        let args = [ os("--sort=size") ];
+        let opts = Options::getopts(&args);
         assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
     }
 
     #[test]
     fn test_sort_name() {
-        let opts = Options::getopts(&[ "--sort=name" ]);
+        let args = [ os("--sort=name") ];
+        let opts = Options::getopts(&args);
         assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
     }
 
     #[test]
     fn test_sort_name_lowercase() {
-        let opts = Options::getopts(&[ "--sort=Name" ]);
+        let args = [ os("--sort=Name") ];
+        let opts = Options::getopts(&args);
         assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
     }
 
     #[test]
     #[cfg(feature="git")]
     fn just_git() {
-        let opts = Options::getopts(&[ "--git" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
+        let args = [ os("--git") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::GIT, false, &flags::LONG))
     }
 
     #[test]
     fn extended_without_long() {
         if xattr::ENABLED {
-            let opts = Options::getopts(&[ "--extended" ]);
-            assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
+            let args = [ os("--extended") ];
+            let opts = Options::getopts(&args);
+            assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::EXTENDED, false, &flags::LONG))
         }
     }
 
     #[test]
     fn level_without_recurse_or_tree() {
-        let opts = Options::getopts(&[ "--level", "69105" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
+        let args = [ os("--level"), os("69105") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))
     }
 
     #[test]
     fn all_all_with_tree() {
-        let opts = Options::getopts(&[ "--all", "--all", "--tree" ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("all --all", true, "tree"))
+        let args = [ os("--all"), os("--all"), os("--tree") ];
+        let opts = Options::getopts(&args);
+        assert_eq!(opts.unwrap_err(), Misfire::TreeAllAll)
     }
 
     #[test]
     fn nowt() {
-        let nothing: Vec<String> = Vec::new();
+        let nothing: Vec<OsString> = Vec::new();
         let dots = Options::getopts(&nothing).unwrap().0.filter.dot_filter;
         assert_eq!(dots, DotFilter::JustFiles);
     }
 
     #[test]
     fn all() {
-        let dots = Options::getopts(&[ "--all".to_string() ]).unwrap().0.filter.dot_filter;
+        let args = [ os("--all") ];
+        let dots = Options::getopts(&args).unwrap().0.filter.dot_filter;
         assert_eq!(dots, DotFilter::Dotfiles);
     }
 
     #[test]
     fn allall() {
-        let dots = Options::getopts(&[ "-a".to_string(), "-a".to_string() ]).unwrap().0.filter.dot_filter;
+        let args = [ os("-a"), os("-a") ];
+        let dots = Options::getopts(&args).unwrap().0.filter.dot_filter;
         assert_eq!(dots, DotFilter::DotfilesAndDots);
     }
 }

+ 68 - 42
src/options/parser.rs

@@ -31,6 +31,7 @@
 #![allow(unused_variables, dead_code)]
 
 use std::ffi::{OsStr, OsString};
+use std::fmt;
 
 
 pub type ShortArg = u8;
@@ -66,63 +67,87 @@ pub enum TakesValue {
 
 #[derive(PartialEq, Debug)]
 pub struct Arg {
-    short: Option<ShortArg>,
-    long: LongArg,
-    takes_value: TakesValue,
+    pub short: Option<ShortArg>,
+    pub long: LongArg,
+    pub takes_value: TakesValue,
+}
+
+impl fmt::Display for Arg {
+    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+        write!(f, "--{}", self.long)?;
+
+        if let Some(short) = self.short {
+            write!(f, " (-{})", short as char)?;
+        }
+
+        Ok(())
+    }
 }
 
 #[derive(PartialEq, Debug)]
-pub struct Args(&'static [&'static Arg]);
+pub struct Args(pub &'static [&'static Arg]);
 
 impl Args {
-    fn lookup_short<'a>(&self, short: ShortArg) -> Result<&Arg, ParseError<'a>> {
+    fn lookup_short<'a>(&self, short: ShortArg) -> Result<&Arg, ParseError> {
         match self.0.into_iter().find(|arg| arg.short == Some(short)) {
             Some(arg)  => Ok(arg),
             None       => Err(ParseError::UnknownShortArgument { attempt: short })
         }
     }
 
-    fn lookup_long<'a>(&self, long: &'a OsStr) -> Result<&Arg, ParseError<'a>> {
+    fn lookup_long<'a>(&self, long: &'a OsStr) -> Result<&Arg, ParseError> {
         match self.0.into_iter().find(|arg| arg.long == long) {
             Some(arg)  => Ok(arg),
-            None       => Err(ParseError::UnknownArgument { attempt: long })
+            None       => Err(ParseError::UnknownArgument { attempt: long.to_os_string() })
         }
     }
 }
 
 
 #[derive(PartialEq, Debug)]
-pub struct Matches<'a> {
+pub struct Matches<'args> {
     /// Long and short arguments need to be kept in the same vector, because
     /// we usually want the one nearest the end to count.
-    flags: Vec<(Flag, Option<&'a OsStr>)>,
-    frees: Vec<&'a OsStr>,
+    pub flags: Vec<(Flag, Option<&'args OsStr>)>,
+    pub frees: Vec<&'args OsStr>,
 }
 
 impl<'a> Matches<'a> {
-    fn has(&self, arg: &Arg) -> bool {
+    pub fn has(&self, arg: &Arg) -> bool {
         self.flags.iter().rev()
             .find(|tuple| tuple.1.is_none() && tuple.0.matches(arg))
             .is_some()
     }
 
-    fn get(&self, arg: &Arg) -> Option<&OsStr> {
+    pub fn get(&self, arg: &Arg) -> Option<&OsStr> {
         self.flags.iter().rev()
             .find(|tuple| tuple.1.is_some() && tuple.0.matches(arg))
             .map(|tuple| tuple.1.unwrap())
     }
+
+    pub fn count(&self, arg: &Arg) -> usize {
+        self.flags.iter()
+            .filter(|tuple| tuple.0.matches(arg))
+            .count()
+    }
 }
 
 #[derive(PartialEq, Debug)]
-pub enum ParseError<'a> {
+pub enum ParseError {
     NeedsValue { flag: Flag },
     ForbiddenValue { flag: Flag },
     UnknownShortArgument { attempt: ShortArg },
-    UnknownArgument { attempt: &'a OsStr },
+    UnknownArgument { attempt: OsString },
 }
 
+// It’s technically possible for ParseError::UnknownArgument to borrow its
+// OsStr rather than owning it, but that would give ParseError a lifetime,
+// which would give Misfire a lifetime, which gets used everywhere. And this
+// only happens when an error occurs, so it’s not really worth it.
 
-fn parse<'a>(args: Args, inputs: &'a [OsString]) -> Result<Matches<'a>, ParseError<'a>> {
+
+pub fn parse<'args, I>(args: &Args, inputs: I) -> Result<Matches<'args>, ParseError>
+where I: IntoIterator<Item=&'args OsString> {
     use std::os::unix::ffi::OsStrExt;
     use self::TakesValue::*;
 
@@ -133,8 +158,8 @@ fn parse<'a>(args: Args, inputs: &'a [OsString]) -> Result<Matches<'a>, ParseErr
         frees: Vec::new(),
     };
 
-    let mut iter = inputs.iter();
-    while let Some(arg) = iter.next() {
+    let mut inputs = inputs.into_iter();
+    while let Some(arg) = inputs.next() {
         let bytes = arg.as_bytes();
 
         if !parsing {
@@ -160,7 +185,7 @@ fn parse<'a>(args: Args, inputs: &'a [OsString]) -> Result<Matches<'a>, ParseErr
                 match arg.takes_value {
                     Forbidden  => results.flags.push((flag, None)),
                     Necessary  => {
-                        if let Some(next_arg) = iter.next() {
+                        if let Some(next_arg) = inputs.next() {
                             results.flags.push((flag, Some(next_arg)));
                         }
                         else {
@@ -203,7 +228,7 @@ fn parse<'a>(args: Args, inputs: &'a [OsString]) -> Result<Matches<'a>, ParseErr
                                 results.flags.push((flag, Some(OsStr::from_bytes(remnants))));
                                 break;
                             }
-                            else if let Some(next_arg) = iter.next() {
+                            else if let Some(next_arg) = inputs.next() {
                                 results.flags.push((flag, Some(next_arg)));
                             }
                             else {
@@ -250,6 +275,7 @@ fn os(input: &'static str) -> OsString {
     os
 }
 
+
 #[cfg(test)]
 mod split_test {
     use super::{split_on_equals, os};
@@ -294,7 +320,7 @@ mod parse_test {
             #[test]
             fn $name() {
                 let bits = $input;
-                let results = parse(Args(TEST_ARGS), &bits);
+                let results = parse(&Args(TEST_ARGS), bits.into_iter());
                 assert_eq!(results, $result);
             }
         };
@@ -309,47 +335,47 @@ mod parse_test {
 
     // Just filenames
     test!(empty:       []           => Ok(Matches { frees: vec![], flags: vec![] }));
-    test!(one_arg:     [os("exa")]  => Ok(Matches { frees: vec![ os("exa").as_os_str() ], flags: vec![] }));
+    test!(one_arg:     [os("exa")]  => Ok(Matches { frees: vec![ &os("exa") ], flags: vec![] }));
 
     // Dashes and double dashes
-    test!(one_dash:    [os("-")]                 => Ok(Matches { frees: vec![ os("-").as_os_str() ],      flags: vec![] }));
-    test!(two_dashes:  [os("--")]                => Ok(Matches { frees: vec![],                           flags: vec![] }));
-    test!(two_file:    [os("--"), os("file")]    => Ok(Matches { frees: vec![ os("file").as_os_str() ],   flags: vec![] }));
-    test!(two_arg_l:   [os("--"), os("--long")]  => Ok(Matches { frees: vec![ os("--long").as_os_str() ], flags: vec![] }));
-    test!(two_arg_s:   [os("--"), os("-l")]      => Ok(Matches { frees: vec![ os("-l").as_os_str() ],     flags: vec![] }));
+    test!(one_dash:    [os("-")]                 => Ok(Matches { frees: vec![ &os("-") ],      flags: vec![] }));
+    test!(two_dashes:  [os("--")]                => Ok(Matches { frees: vec![],                flags: vec![] }));
+    test!(two_file:    [os("--"), os("file")]    => Ok(Matches { frees: vec![ &os("file") ],   flags: vec![] }));
+    test!(two_arg_l:   [os("--"), os("--long")]  => Ok(Matches { frees: vec![ &os("--long") ], flags: vec![] }));
+    test!(two_arg_s:   [os("--"), os("-l")]      => Ok(Matches { frees: vec![ &os("-l") ],     flags: vec![] }));
 
 
     // Long args
-    test!(long:        [os("--long")]                  => Ok(Matches { frees: vec![],                      flags: vec![ (Flag::Long("long"), None) ] }));
-    test!(long_then:   [os("--long"), os("4")]         => Ok(Matches { frees: vec![ os("4").as_os_str() ], flags: vec![ (Flag::Long("long"), None) ] }));
-    test!(long_two:    [os("--long"), os("--verbose")] => Ok(Matches { frees: vec![],                      flags: vec![ (Flag::Long("long"), None), (Flag::Long("verbose"), None) ] }));
+    test!(long:        [os("--long")]                  => Ok(Matches { frees: vec![],           flags: vec![ (Flag::Long("long"), None) ] }));
+    test!(long_then:   [os("--long"), os("4")]         => Ok(Matches { frees: vec![ &os("4") ], flags: vec![ (Flag::Long("long"), None) ] }));
+    test!(long_two:    [os("--long"), os("--verbose")] => Ok(Matches { frees: vec![],           flags: vec![ (Flag::Long("long"), None), (Flag::Long("verbose"), None) ] }));
 
     // Long args with values
     test!(bad_equals:  [os("--long=equals")]    => Err(ParseError::ForbiddenValue { flag: Flag::Long("long") }));
     test!(no_arg:      [os("--count")]          => Err(ParseError::NeedsValue     { flag: Flag::Long("count") }));
-    test!(arg_equals:  [os("--count=4")]        => Ok(Matches { frees: vec![], flags: vec![ (Flag::Long("count"), Some(os("4").as_os_str())) ] }));
-    test!(arg_then:    [os("--count"), os("4")] => Ok(Matches { frees: vec![], flags: vec![ (Flag::Long("count"), Some(os("4").as_os_str())) ] }));
+    test!(arg_equals:  [os("--count=4")]        => Ok(Matches { frees: vec![], flags: vec![ (Flag::Long("count"), Some(&*os("4"))) ] }));
+    test!(arg_then:    [os("--count"), os("4")] => Ok(Matches { frees: vec![], flags: vec![ (Flag::Long("count"), Some(&*os("4"))) ] }));
 
 
     // Short args
-    test!(short:       [os("-l")]                  => Ok(Matches { frees: vec![],                      flags: vec![ (Flag::Short(b'l'), None) ] }));
-    test!(short_then:  [os("-l"), os("4")]         => Ok(Matches { frees: vec![ os("4").as_os_str() ], flags: vec![ (Flag::Short(b'l'), None) ] }));
-    test!(short_two:   [os("-lv")]                 => Ok(Matches { frees: vec![],                      flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'v'), None) ] }));
-    test!(mixed:       [os("-v"), os("--long")]    => Ok(Matches { frees: vec![],                      flags: vec![ (Flag::Short(b'v'), None), (Flag::Long("long"), None) ] }));
+    test!(short:       [os("-l")]                  => Ok(Matches { frees: vec![],            flags: vec![ (Flag::Short(b'l'), None) ] }));
+    test!(short_then:  [os("-l"), os("4")]         => Ok(Matches { frees: vec![ &*os("4") ], flags: vec![ (Flag::Short(b'l'), None) ] }));
+    test!(short_two:   [os("-lv")]                 => Ok(Matches { frees: vec![],            flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'v'), None) ] }));
+    test!(mixed:       [os("-v"), os("--long")]    => Ok(Matches { frees: vec![],            flags: vec![ (Flag::Short(b'v'), None), (Flag::Long("long"), None) ] }));
 
     // Short args with values
     test!(bad_short:          [os("-l=equals")]       => Err(ParseError::ForbiddenValue { flag: Flag::Short(b'l') }));
     test!(short_none:         [os("-c")]              => Err(ParseError::NeedsValue     { flag: Flag::Short(b'c') }));
-    test!(short_arg_eq:       [os("-c=4")]            => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'c'), Some(os("4").as_os_str())) ] }));
-    test!(short_arg_then:     [os("-c"), os("4")]     => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'c'), Some(os("4").as_os_str())) ] }));
-    test!(short_two_together: [os("-lctwo")]          => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(os("two").as_os_str())) ] }));
-    test!(short_two_equals:   [os("-lc=two")]         => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(os("two").as_os_str())) ] }));
-    test!(short_two_next:     [os("-lc"), os("two")]  => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(os("two").as_os_str())) ] }));
+    test!(short_arg_eq:       [os("-c=4")]            => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'c'), Some(&*os("4"))) ] }));
+    test!(short_arg_then:     [os("-c"), os("4")]     => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'c'), Some(&*os("4"))) ] }));
+    test!(short_two_together: [os("-lctwo")]          => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(&*os("two"))) ] }));
+    test!(short_two_equals:   [os("-lc=two")]         => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(&*os("two"))) ] }));
+    test!(short_two_next:     [os("-lc"), os("two")]  => Ok(Matches { frees: vec![], flags: vec![ (Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(&*os("two"))) ] }));
 
 
     // Unknown args
-    test!(unknown_long:          [os("--quiet")]      => Err(ParseError::UnknownArgument      { attempt: os("quiet").as_os_str() }));
-    test!(unknown_long_eq:       [os("--quiet=shhh")] => Err(ParseError::UnknownArgument      { attempt: os("quiet").as_os_str() }));
+    test!(unknown_long:          [os("--quiet")]      => Err(ParseError::UnknownArgument      { attempt: os("quiet") }));
+    test!(unknown_long_eq:       [os("--quiet=shhh")] => Err(ParseError::UnknownArgument      { attempt: os("quiet") }));
     test!(unknown_short:         [os("-q")]           => Err(ParseError::UnknownShortArgument { attempt: b'q' }));
     test!(unknown_short_2nd:     [os("-lq")]          => Err(ParseError::UnknownShortArgument { attempt: b'q' }));
     test!(unknown_short_eq:      [os("-q=shhh")]      => Err(ParseError::UnknownShortArgument { attempt: b'q' }));

+ 100 - 79
src/options/view.rs

@@ -1,21 +1,22 @@
 use std::env::var_os;
 
-use getopts;
-
-use info::filetype::FileExtensions;
 use output::Colours;
 use output::{View, Mode, grid, details};
 use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions};
 use output::file_name::{Classify, FileStyle};
 use output::time::TimeFormat;
-use options::Misfire;
+
+use options::{flags, Misfire};
+use options::parser::Matches;
+
 use fs::feature::xattr;
+use info::filetype::FileExtensions;
 
 
 impl View {
 
     /// Determine which view to use and all of that view’s arguments.
-    pub fn deduce(matches: &getopts::Matches) -> Result<View, Misfire> {
+    pub fn deduce(matches: &Matches) -> Result<View, Misfire> {
         let mode = Mode::deduce(matches)?;
         let colours = Colours::deduce(matches)?;
         let style = FileStyle::deduce(matches);
@@ -27,40 +28,41 @@ impl View {
 impl Mode {
 
     /// Determine the mode from the command-line arguments.
-    pub fn deduce(matches: &getopts::Matches) -> Result<Mode, Misfire> {
+    pub fn deduce(matches: &Matches) -> Result<Mode, Misfire> {
         use options::misfire::Misfire::*;
 
         let long = || {
-            if matches.opt_present("across") && !matches.opt_present("grid") {
-                Err(Useless("across", true, "long"))
+            if matches.has(&flags::ACROSS) && !matches.has(&flags::GRID) {
+                Err(Useless(&flags::ACROSS, true, &flags::LONG))
             }
-            else if matches.opt_present("oneline") {
-                Err(Useless("oneline", true, "long"))
+            else if matches.has(&flags::ONE_LINE) {
+                Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
             }
             else {
                 Ok(details::Options {
                     table: Some(TableOptions::deduce(matches)?),
-                    header: matches.opt_present("header"),
-                    xattr: xattr::ENABLED && matches.opt_present("extended"),
+                    header: matches.has(&flags::HEADER),
+                    xattr: xattr::ENABLED && matches.has(&flags::EXTENDED),
                 })
             }
         };
 
         let long_options_scan = || {
-            for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
-                if matches.opt_present(option) {
-                    return Err(Useless(option, false, "long"));
+            for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
+                             &flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
+                if matches.has(option) {
+                    return Err(Useless(*option, false, &flags::LONG));
                 }
             }
 
-            if cfg!(feature="git") && matches.opt_present("git") {
-                Err(Useless("git", false, "long"))
+            if cfg!(feature="git") && matches.has(&flags::GIT) {
+                Err(Useless(&flags::GIT, false, &flags::LONG))
             }
-            else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
-                Err(Useless2("level", "recurse", "tree"))
+            else if matches.has(&flags::LEVEL) && !matches.has(&flags::RECURSE) && !matches.has(&flags::TREE) {
+                Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))
             }
-            else if xattr::ENABLED && matches.opt_present("extended") {
-                Err(Useless("extended", false, "long"))
+            else if xattr::ENABLED && matches.has(&flags::EXTENDED) {
+                Err(Useless(&flags::EXTENDED, false, &flags::LONG))
             }
             else {
                 Ok(())
@@ -69,15 +71,15 @@ impl Mode {
 
         let other_options_scan = || {
             if let Some(width) = TerminalWidth::deduce()?.width() {
-                if matches.opt_present("oneline") {
-                    if matches.opt_present("across") {
-                        Err(Useless("across", true, "oneline"))
+                if matches.has(&flags::ONE_LINE) {
+                    if matches.has(&flags::ACROSS) {
+                        Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE))
                     }
                     else {
                         Ok(Mode::Lines)
                     }
                 }
-                else if matches.opt_present("tree") {
+                else if matches.has(&flags::TREE) {
                     let details = details::Options {
                         table: None,
                         header: false,
@@ -88,7 +90,7 @@ impl Mode {
                 }
                 else {
                     let grid = grid::Options {
-                        across: matches.opt_present("across"),
+                        across: matches.has(&flags::ACROSS),
                         console_width: width,
                     };
 
@@ -100,7 +102,7 @@ impl Mode {
                 // as the program’s stdout being connected to a file, then
                 // fallback to the lines view.
 
-                if matches.opt_present("tree") {
+                if matches.has(&flags::TREE) {
                     let details = details::Options {
                         table: None,
                         header: false,
@@ -115,9 +117,9 @@ impl Mode {
             }
         };
 
-        if matches.opt_present("long") {
+        if matches.has(&flags::LONG) {
             let details = long()?;
-            if matches.opt_present("grid") {
+            if matches.has(&flags::GRID) {
                 match other_options_scan()? {
                     Mode::Grid(grid)  => return Ok(Mode::GridDetails(grid, details)),
                     others            => return Ok(others),
@@ -180,17 +182,17 @@ impl TerminalWidth {
 
 
 impl TableOptions {
-    fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire> {
+    fn deduce(matches: &Matches) -> Result<Self, Misfire> {
         Ok(TableOptions {
             env:         Environment::load_all(),
             time_format: TimeFormat::deduce(matches)?,
             size_format: SizeFormat::deduce(matches)?,
             time_types:  TimeTypes::deduce(matches)?,
-            inode:  matches.opt_present("inode"),
-            links:  matches.opt_present("links"),
-            blocks: matches.opt_present("blocks"),
-            group:  matches.opt_present("group"),
-            git:    cfg!(feature="git") && matches.opt_present("git"),
+            inode:  matches.has(&flags::INODE),
+            links:  matches.has(&flags::LINKS),
+            blocks: matches.has(&flags::BLOCKS),
+            group:  matches.has(&flags::GROUP),
+            git:    cfg!(feature="git") && matches.has(&flags::GIT),
         })
     }
 }
@@ -206,12 +208,12 @@ impl SizeFormat {
     /// strings of digits in your head. Changing the format to anything else
     /// involves the `--binary` or `--bytes` flags, and these conflict with
     /// each other.
-    fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
-        let binary = matches.opt_present("binary");
-        let bytes  = matches.opt_present("bytes");
+    fn deduce(matches: &Matches) -> Result<SizeFormat, Misfire> {
+        let binary = matches.has(&flags::BINARY);
+        let bytes  = matches.has(&flags::BYTES);
 
         match (binary, bytes) {
-            (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
+            (true,  true )  => Err(Misfire::Conflict(&flags::BINARY, &flags::BYTES)),
             (true,  false)  => Ok(SizeFormat::BinaryBytes),
             (false, true )  => Ok(SizeFormat::JustBytes),
             (false, false)  => Ok(SizeFormat::DecimalBytes),
@@ -223,21 +225,29 @@ impl SizeFormat {
 impl TimeFormat {
 
     /// Determine how time should be formatted in timestamp columns.
-    fn deduce(matches: &getopts::Matches) -> Result<TimeFormat, Misfire> {
+    fn deduce(matches: &Matches) -> Result<TimeFormat, Misfire> {
         pub use output::time::{DefaultFormat, ISOFormat};
         const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
 
-        if let Some(word) = matches.opt_str("time-style") {
-            match &*word {
-                "default"   => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
-                "iso"       => Ok(TimeFormat::ISOFormat(ISOFormat::new())),
-                "long-iso"  => Ok(TimeFormat::LongISO),
-                "full-iso"  => Ok(TimeFormat::FullISO),
-                otherwise   => Err(Misfire::bad_argument("time-style", otherwise, STYLES)),
-            }
+        let word = match matches.get(&flags::TIME_STYLE) {
+            Some(w) => w,
+            None    => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
+        };
+
+        if word == "default" {
+            Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
+        }
+        else if word == "iso" {
+            Ok(TimeFormat::ISOFormat(ISOFormat::new()))
+        }
+        else if word == "long-iso" {
+            Ok(TimeFormat::LongISO)
+        }
+        else if word == "full-iso" {
+            Ok(TimeFormat::FullISO)
         }
         else {
-            Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
+            Err(Misfire::bad_argument(&flags::TIME_STYLE, word, STYLES))
         }
     }
 }
@@ -255,29 +265,35 @@ impl TimeTypes {
     /// It’s valid to show more than one column by passing in more than one
     /// option, but passing *no* options means that the user just wants to
     /// see the default set.
-    fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
-        let possible_word = matches.opt_str("time");
-        let modified = matches.opt_present("modified");
-        let created  = matches.opt_present("created");
-        let accessed = matches.opt_present("accessed");
+    fn deduce(matches: &Matches) -> Result<TimeTypes, Misfire> {
+        let possible_word = matches.get(&flags::TIME);
+        let modified = matches.has(&flags::MODIFIED);
+        let created  = matches.has(&flags::CREATED);
+        let accessed = matches.has(&flags::ACCESSED);
 
         if let Some(word) = possible_word {
             if modified {
-                return Err(Misfire::Useless("modified", true, "time"));
+                return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME));
             }
             else if created {
-                return Err(Misfire::Useless("created", true, "time"));
+                return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME));
             }
             else if accessed {
-                return Err(Misfire::Useless("accessed", true, "time"));
+                return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME));
             }
 
-            static TIMES: &[& str] = &["modified", "accessed", "created"];
-            match &*word {
-                "mod" | "modified"  => Ok(TimeTypes { accessed: false, modified: true,  created: false }),
-                "acc" | "accessed"  => Ok(TimeTypes { accessed: true,  modified: false, created: false }),
-                "cr"  | "created"   => Ok(TimeTypes { accessed: false, modified: false, created: true  }),
-                otherwise           => Err(Misfire::bad_argument("time", otherwise, TIMES))
+            static TIMES: &[&str] = &["modified", "accessed", "created"];
+            if word == "mod" || word == "modified" {
+                Ok(TimeTypes { accessed: false, modified: true,  created: false })
+            }
+            else if word == "acc" || word == "accessed" {
+                Ok(TimeTypes { accessed: true,  modified: false, created: false })
+            }
+            else if word == "cr" || word == "created" {
+                Ok(TimeTypes { accessed: false, modified: false, created: true  })
+            }
+            else {
+                Err(Misfire::bad_argument(&flags::TIME, word, TIMES))
             }
         }
         else if modified || created || accessed {
@@ -319,31 +335,37 @@ impl Default for TerminalColours {
 impl TerminalColours {
 
     /// Determine which terminal colour conditions to use.
-    fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
+    fn deduce(matches: &Matches) -> Result<TerminalColours, Misfire> {
         const COLOURS: &[&str] = &["always", "auto", "never"];
 
-        if let Some(word) = matches.opt_str("color").or_else(|| matches.opt_str("colour")) {
-            match &*word {
-                "always"              => Ok(TerminalColours::Always),
-                "auto" | "automatic"  => Ok(TerminalColours::Automatic),
-                "never"               => Ok(TerminalColours::Never),
-                otherwise             => Err(Misfire::bad_argument("color", otherwise, COLOURS))
-            }
+        let word = match matches.get(&flags::COLOR).or_else(|| matches.get(&flags::COLOUR)) {
+            Some(w) => w,
+            None    => return Ok(TerminalColours::default()),
+        };
+
+        if word == "always" {
+            Ok(TerminalColours::Always)
+        }
+        else if word == "auto" || word == "automatic" {
+            Ok(TerminalColours::Automatic)
+        }
+        else if word == "never" {
+            Ok(TerminalColours::Never)
         }
         else {
-            Ok(TerminalColours::default())
+            Err(Misfire::bad_argument(&flags::COLOR, word, COLOURS))
         }
     }
 }
 
 
 impl Colours {
-    fn deduce(matches: &getopts::Matches) -> Result<Colours, Misfire> {
+    fn deduce(matches: &Matches) -> Result<Colours, Misfire> {
         use self::TerminalColours::*;
 
         let tc = TerminalColours::deduce(matches)?;
         if tc == Always || (tc == Automatic && TERM_WIDTH.is_some()) {
-            let scale = matches.opt_present("color-scale") || matches.opt_present("colour-scale");
+            let scale = matches.has(&flags::COLOR_SCALE) || matches.has(&flags::COLOUR_SCALE);
             Ok(Colours::colourful(scale))
         }
         else {
@@ -355,18 +377,17 @@ impl Colours {
 
 
 impl FileStyle {
-    fn deduce(matches: &getopts::Matches) -> FileStyle {
+    fn deduce(matches: &Matches) -> FileStyle {
         let classify = Classify::deduce(matches);
         let exts = FileExtensions;
         FileStyle { classify, exts }
     }
-
 }
 
 impl Classify {
-    fn deduce(matches: &getopts::Matches) -> Classify {
-        if matches.opt_present("classify") { Classify::AddFileIndicators }
-                                      else { Classify::JustFilenames }
+    fn deduce(matches: &Matches) -> Classify {
+        if matches.has(&flags::CLASSIFY) { Classify::AddFileIndicators }
+                                    else { Classify::JustFilenames }
     }
 }
 

+ 1 - 1
xtests/run.sh

@@ -78,7 +78,7 @@ COLUMNS=80 $exa $testcases/file-names -R 2>&1 | diff -q - $results/file_names_R
            $exa $testcases/file-names -T 2>&1 | diff -q - $results/file_names_T || exit 1
 
 # At least make sure it handles invalid UTF-8 arguments without crashing
-$exa $testcases/file-names/* 2>/dev/null
+$exa $testcases/file-names/* >/dev/null || exit 1
 
 
 # Sorting and extension file types