Ver Fonte

Split up the options module

The original options was becoming a bit unwieldy, and would have been even more so if I added the same amount of comments. So this commit splits it up.

There's no extra hiding going on here, or rearranging things within the module: (almost) everything now has to be marked 'pub' to let other sub-modules in the new options module to see it.
Benjamin Sago há 9 anos atrás
pai
commit
e9e1161cec
7 ficheiros alterados com 1078 adições e 883 exclusões
  1. 0 883
      src/options.rs
  2. 107 0
      src/options/dir_action.rs
  3. 228 0
      src/options/filter.rs
  4. 37 0
      src/options/help.rs
  5. 69 0
      src/options/misfire.rs
  6. 282 0
      src/options/mod.rs
  7. 355 0
      src/options/view.rs

+ 0 - 883
src/options.rs

@@ -1,883 +0,0 @@
-use std::cmp;
-use std::env::var_os;
-use std::fmt;
-use std::num::ParseIntError;
-use std::os::unix::fs::MetadataExt;
-
-use getopts;
-use natord;
-
-use fs::feature::xattr;
-use fs::File;
-use output::{Grid, Details, GridDetails, Lines};
-use output::Colours;
-use output::column::{Columns, TimeTypes, SizeFormat};
-use term::dimensions;
-
-
-/// These **options** represent a parsed, error-checked versions of the
-/// user's command-line options.
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub struct Options {
-
-    /// The action to perform when encountering a directory rather than a
-    /// regular file.
-    pub dir_action: DirAction,
-
-    /// How to sort and filter files before outputting them.
-    pub filter: FileFilter,
-
-    /// The type of output to use (lines, grid, or details).
-    pub view: View,
-}
-
-impl Options {
-
-    /// Call getopts on the given slice of command-line strings.
-    #[allow(unused_results)]
-    pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
-        let mut opts = getopts::Options::new();
-
-        opts.optflag("v", "version",   "display 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("G", "grid",      "display entries in a grid view (default)");
-        opts.optflag("l", "long",      "display extended details and attributes");
-        opts.optflag("R", "recurse",   "recurse into directories");
-        opts.optflag("T", "tree",      "recurse into subdirectories in a tree view");
-        opts.optflag("x", "across",    "sort multi-column view entries across");
-        opts.optopt ("",  "color",     "when to show anything in colours", "WHEN");
-        opts.optopt ("",  "colour",    "when to show anything in colours (alternate spelling)", "WHEN");
-
-        // Filtering and sorting options
-        opts.optflag("",  "group-directories-first", "list directories before other files");
-        opts.optflag("a", "all",       "show dot-files");
-        opts.optflag("d", "list-dirs", "list directories as regular files");
-        opts.optflag("r", "reverse",   "reverse order of files");
-        opts.optopt ("s", "sort",      "field to sort by", "WORD");
-
-        // Long view options
-        opts.optflag("b", "binary",    "use binary prefixes in file sizes");
-        opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
-        opts.optflag("g", "group",     "show group as well as user");
-        opts.optflag("h", "header",    "show a header row at the top");
-        opts.optflag("H", "links",     "show number of hard links");
-        opts.optflag("i", "inode",     "show each file's inode number");
-        opts.optopt ("L", "level",     "maximum depth of recursion", "DEPTH");
-        opts.optflag("m", "modified",  "display timestamp of most recent modification");
-        opts.optflag("S", "blocks",    "show number of file system blocks");
-        opts.optopt ("t", "time",      "which timestamp to show for a file", "WORD");
-        opts.optflag("u", "accessed",  "display timestamp of last access for a file");
-        opts.optflag("U", "created",   "display timestamp of creation for a file");
-
-        if cfg!(feature="git") {
-            opts.optflag("", "git", "show git status");
-        }
-
-        if xattr::ENABLED {
-            opts.optflag("@", "extended", "display extended attribute keys and sizes");
-        }
-
-        let matches = match opts.parse(args) {
-            Ok(m)   => m,
-            Err(e)  => return Err(Misfire::InvalidOptions(e)),
-        };
-
-        if matches.opt_present("help") {
-            let mut help_string = "Usage:\n  exa [options] [files...]\n".to_owned();
-
-            if !matches.opt_present("long") {
-                help_string.push_str(OPTIONS);
-            }
-
-            help_string.push_str(LONG_OPTIONS);
-
-            if cfg!(feature="git") {
-                help_string.push_str(GIT_HELP);
-                help_string.push('\n');
-            }
-
-            if xattr::ENABLED {
-                help_string.push_str(EXTENDED_HELP);
-                help_string.push('\n');
-            }
-
-            return Err(Misfire::Help(help_string));
-        }
-        else if matches.opt_present("version") {
-            return Err(Misfire::Version);
-        }
-
-        let options = try!(Options::deduce(&matches));
-        Ok((options, matches.free))
-    }
-
-    /// Whether the View specified in this set of options includes a Git
-    /// status column. It's only worth trying to discover a repository if the
-    /// results will end up being displayed.
-    pub fn should_scan_for_git(&self) -> bool {
-        match self.view {
-            View::Details(Details { columns: Some(cols), .. }) => cols.should_scan_for_git(),
-            View::GridDetails(GridDetails { details: Details { columns: Some(cols), .. }, .. }) => cols.should_scan_for_git(),
-            _ => false,
-        }
-    }
-
-    fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
-        let dir_action = try!(DirAction::deduce(&matches));
-        let filter = try!(FileFilter::deduce(&matches));
-        let view = try!(View::deduce(&matches, filter, dir_action));
-
-        Ok(Options {
-            dir_action: dir_action,
-            view:       view,
-            filter:     filter,
-        })
-    }
-}
-
-
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum View {
-    Details(Details),
-    Grid(Grid),
-    GridDetails(GridDetails),
-    Lines(Lines),
-}
-
-impl View {
-    fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
-        use self::Misfire::*;
-
-        let long = || {
-            if matches.opt_present("across") && !matches.opt_present("grid") {
-                Err(Useless("across", true, "long"))
-            }
-            else if matches.opt_present("oneline") {
-                Err(Useless("oneline", true, "long"))
-            }
-            else {
-                let term_colours = try!(TerminalColours::deduce(matches));
-                let colours = match term_colours {
-                    TerminalColours::Always    => Colours::colourful(),
-                    TerminalColours::Never     => Colours::plain(),
-                    TerminalColours::Automatic => {
-                        if dimensions().is_some() {
-                            Colours::colourful()
-                        }
-                        else {
-                            Colours::plain()
-                        }
-                    },
-                };
-
-                let details = Details {
-                    columns: Some(try!(Columns::deduce(matches))),
-                    header: matches.opt_present("header"),
-                    recurse: dir_action.recurse_options(),
-                    filter: filter,
-                    xattr: xattr::ENABLED && matches.opt_present("extended"),
-                    colours: colours,
-                };
-
-                Ok(details)
-            }
-        };
-
-        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"));
-                }
-            }
-
-            if cfg!(feature="git") && matches.opt_present("git") {
-                Err(Useless("git", false, "long"))
-            }
-            else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
-                Err(Useless2("level", "recurse", "tree"))
-            }
-            else if xattr::ENABLED && matches.opt_present("extended") {
-                Err(Useless("extended", false, "long"))
-            }
-            else {
-                Ok(())
-            }
-        };
-
-        let other_options_scan = || {
-            let term_colours = try!(TerminalColours::deduce(matches));
-            let term_width   = try!(TerminalWidth::deduce(matches));
-
-            if let TerminalWidth::Set(width) = term_width {
-                let colours = match term_colours {
-                    TerminalColours::Always    => Colours::colourful(),
-                    TerminalColours::Never     => Colours::plain(),
-                    TerminalColours::Automatic => Colours::colourful(),
-                };
-
-                if matches.opt_present("oneline") {
-                    if matches.opt_present("across") {
-                        Err(Useless("across", true, "oneline"))
-                    }
-                    else {
-                        let lines = Lines {
-                             colours: colours,
-                        };
-
-                        Ok(View::Lines(lines))
-                    }
-                }
-                else if matches.opt_present("tree") {
-                    let details = Details {
-                        columns: None,
-                        header: false,
-                        recurse: dir_action.recurse_options(),
-                        filter: filter,
-                        xattr: false,
-                        colours: colours,
-                    };
-
-                    Ok(View::Details(details))
-                }
-                else {
-                    let grid = Grid {
-                        across: matches.opt_present("across"),
-                        console_width: width,
-                        colours: colours,
-                    };
-
-                    Ok(View::Grid(grid))
-                }
-            }
-            else {
-                // If the terminal width couldn’t be matched for some reason, such
-                // as the program’s stdout being connected to a file, then
-                // fallback to the lines view.
-
-                let colours = match term_colours {
-                    TerminalColours::Always    => Colours::colourful(),
-                    TerminalColours::Never     => Colours::plain(),
-                    TerminalColours::Automatic => Colours::plain(),
-                };
-
-                if matches.opt_present("tree") {
-                    let details = Details {
-                        columns: None,
-                        header: false,
-                        recurse: dir_action.recurse_options(),
-                        filter: filter,
-                        xattr: false,
-                        colours: colours,
-                    };
-
-                    Ok(View::Details(details))
-                }
-                else {
-                    let lines = Lines {
-                         colours: colours,
-                    };
-
-                    Ok(View::Lines(lines))
-                }
-            }
-        };
-
-        if matches.opt_present("long") {
-            let long_options = try!(long());
-
-            if matches.opt_present("grid") {
-                match other_options_scan() {
-                    Ok(View::Grid(grid)) => return Ok(View::GridDetails(GridDetails { grid: grid, details: long_options })),
-                    Ok(lines)            => return Ok(lines),
-                    Err(e)               => return Err(e),
-                };
-            }
-            else {
-                return Ok(View::Details(long_options));
-            }
-        }
-
-        try!(long_options_scan());
-
-        other_options_scan()
-    }
-}
-
-
-/// The **file filter** processes a vector of files before outputting them,
-/// filtering and sorting the files depending on the user’s command-line
-/// flags.
-#[derive(Default, PartialEq, Debug, Copy, Clone)]
-pub struct FileFilter {
-    list_dirs_first: bool,
-    reverse: bool,
-    show_invisibles: bool,
-    sort_field: SortField,
-}
-
-impl FileFilter {
-    fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
-        let sort_field = try!(SortField::deduce(&matches));
-
-        Ok(FileFilter {
-            list_dirs_first: matches.opt_present("group-directories-first"),
-            reverse:         matches.opt_present("reverse"),
-            show_invisibles: matches.opt_present("all"),
-            sort_field:      sort_field,
-        })
-    }
-
-    /// Remove every file in the given vector that does *not* pass the
-    /// filter predicate.
-    pub fn filter_files(&self, files: &mut Vec<File>) {
-        if !self.show_invisibles {
-            files.retain(|f| !f.is_dotfile());
-        }
-    }
-
-    /// Sort the files in the given vector based on the sort field option.
-    pub fn sort_files<'_, F>(&self, files: &mut Vec<F>)
-    where F: AsRef<File<'_>> {
-
-        files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
-
-        if self.reverse {
-            files.reverse();
-        }
-
-        if self.list_dirs_first {
-            // This relies on the fact that `sort_by` is stable.
-            files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
-        }
-    }
-
-    pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
-        use self::SortCase::{Sensitive, Insensitive};
-
-        match self.sort_field {
-            SortField::Unsorted  => cmp::Ordering::Equal,
-
-            SortField::Name(Sensitive)    => natord::compare(&a.name, &b.name),
-            SortField::Name(Insensitive)  => natord::compare_ignore_case(&a.name, &b.name),
-
-            SortField::Size          => a.metadata.len().cmp(&b.metadata.len()),
-            SortField::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
-            SortField::ModifiedDate  => a.metadata.mtime().cmp(&b.metadata.mtime()),
-            SortField::AccessedDate  => a.metadata.atime().cmp(&b.metadata.atime()),
-            SortField::CreatedDate   => a.metadata.ctime().cmp(&b.metadata.ctime()),
-
-            SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
-                cmp::Ordering::Equal  => natord::compare(&*a.name, &*b.name),
-                order                 => order,
-            },
-
-            SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
-                cmp::Ordering::Equal  => natord::compare_ignore_case(&*a.name, &*b.name),
-                order                 => order,
-            },
-        }
-    }
-}
-
-
-/// What to do when encountering a directory?
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum DirAction {
-    AsFile,
-    List,
-    Recurse(RecurseOptions),
-}
-
-impl DirAction {
-    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");
-
-        match (recurse, list, tree) {
-            (true,  true,  _    )  => Err(Misfire::Conflict("recurse", "list-dirs")),
-            (_,     true,  true )  => Err(Misfire::Conflict("tree", "list-dirs")),
-            (true,  false, false)  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
-            (_   ,  _,     true )  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
-            (false, true,  _    )  => Ok(DirAction::AsFile),
-            (false, false, _    )  => Ok(DirAction::List),
-        }
-    }
-
-    pub fn recurse_options(&self) -> Option<RecurseOptions> {
-        match *self {
-            DirAction::Recurse(opts) => Some(opts),
-            _ => None,
-        }
-    }
-
-    pub fn treat_dirs_as_files(&self) -> bool {
-        match *self {
-            DirAction::AsFile => true,
-            DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
-            _ => false,
-        }
-    }
-}
-
-
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub struct RecurseOptions {
-    pub tree:      bool,
-    pub max_depth: Option<usize>,
-}
-
-impl RecurseOptions {
-    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() {
-                Ok(l)  => Some(l),
-                Err(e) => return Err(Misfire::FailedParse(e)),
-            }
-        }
-        else {
-            None
-        };
-
-        Ok(RecurseOptions {
-            tree: tree,
-            max_depth: max_depth,
-        })
-    }
-
-    pub fn is_too_deep(&self, depth: usize) -> bool {
-        match self.max_depth {
-            None    => false,
-            Some(d) => {
-                d <= depth
-            }
-        }
-    }
-}
-
-
-/// User-supplied field to sort by.
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum SortField {
-    Unsorted,
-    Name(SortCase), Extension(SortCase),
-    Size, FileInode,
-    ModifiedDate, AccessedDate, CreatedDate,
-}
-
-/// Whether a field should be sorted case-sensitively or case-insensitively.
-///
-/// This determines which of the `natord` functions to use.
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum SortCase {
-    Sensitive,
-    Insensitive,
-}
-
-impl Default for SortField {
-    fn default() -> SortField {
-        SortField::Name(SortCase::Sensitive)
-    }
-}
-
-impl SortField {
-    fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
-        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),
-                "none"                => Ok(SortField::Unsorted),
-                "inode"               => Ok(SortField::FileInode),
-                field                 => Err(Misfire::bad_argument("sort", field))
-            }
-        }
-        else {
-            Ok(SortField::default())
-        }
-    }
-}
-
-
-#[derive(PartialEq, Debug)]
-enum TerminalWidth {
-    Set(usize),
-    Unset,
-}
-
-impl TerminalWidth {
-    fn deduce(_: &getopts::Matches) -> Result<TerminalWidth, Misfire> {
-        if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
-            match columns.parse() {
-                Ok(width)  => Ok(TerminalWidth::Set(width)),
-                Err(e)     => Err(Misfire::FailedParse(e)),
-            }
-        }
-        else if let Some((width, _)) = dimensions() {
-            Ok(TerminalWidth::Set(width))
-        }
-        else {
-            Ok(TerminalWidth::Unset)
-        }
-    }
-}
-
-
-#[derive(PartialEq, Debug)]
-enum TerminalColours {
-    Always,
-    Automatic,
-    Never,
-}
-
-impl Default for TerminalColours {
-    fn default() -> TerminalColours {
-        TerminalColours::Automatic
-    }
-}
-
-impl TerminalColours {
-    fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
-        if let Some(word) = matches.opt_str("color").or(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))
-            }
-        }
-        else {
-            Ok(TerminalColours::default())
-        }
-    }
-}
-
-
-impl Columns {
-    fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
-        Ok(Columns {
-            size_format: try!(SizeFormat::deduce(matches)),
-            time_types:  try!(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"),
-        })
-    }
-}
-
-
-impl SizeFormat {
-
-    /// Determine which file size to use in the file size column based on
-    /// the user’s options.
-    ///
-    /// The default mode is to use the decimal prefixes, as they are the
-    /// most commonly-understood, and don’t involve trying to parse large
-    /// 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");
-
-        match (binary, bytes) {
-            (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
-            (true,  false)  => Ok(SizeFormat::BinaryBytes),
-            (false, true )  => Ok(SizeFormat::JustBytes),
-            (false, false)  => Ok(SizeFormat::DecimalBytes),
-        }
-    }
-}
-
-
-impl TimeTypes {
-
-    /// Determine which of a file’s time fields should be displayed for it
-    /// based on the user’s options.
-    ///
-    /// There are two separate ways to pick which fields to show: with a
-    /// flag (such as `--modified`) or with a parameter (such as
-    /// `--time=modified`). An error is signaled if both ways are used.
-    ///
-    /// 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");
-
-        if let Some(word) = possible_word {
-            if modified {
-                return Err(Misfire::Useless("modified", true, "time"));
-            }
-            else if created {
-                return Err(Misfire::Useless("created", true, "time"));
-            }
-            else if accessed {
-                return Err(Misfire::Useless("accessed", true, "time"));
-            }
-
-            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)),
-            }
-        }
-        else if modified || created || accessed {
-            Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
-        }
-        else {
-            Ok(TimeTypes::default())
-        }
-    }
-}
-
-
-/// One of these things could happen instead of listing files.
-#[derive(PartialEq, Debug)]
-pub enum Misfire {
-
-    /// The getopts crate didn't like these arguments.
-    InvalidOptions(getopts::Fail),
-
-    /// The user asked for help. This isn't strictly an error, which is why
-    /// this enum isn't named Error!
-    Help(String),
-
-    /// The user wanted the version number.
-    Version,
-
-    /// Two options were given that conflict with one another.
-    Conflict(&'static str, &'static str),
-
-    /// An option was given that does nothing when another one either is or
-    /// isn't present.
-    Useless(&'static str, bool, &'static str),
-
-    /// An option was given that does nothing when either of two other options
-    /// are not present.
-    Useless2(&'static str, &'static str, &'static str),
-
-    /// A numeric option was given that failed to be parsed as a number.
-    FailedParse(ParseIntError),
-}
-
-impl Misfire {
-
-    /// The OS return code this misfire should signify.
-    pub fn error_code(&self) -> i32 {
-        if let Misfire::Help(_) = *self { 2 }
-                                   else { 3 }
-    }
-
-    /// The Misfire that happens when an option gets given the wrong
-    /// 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) -> Misfire {
-        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--{} {}", option, otherwise)))
-    }
-}
-
-impl fmt::Display for Misfire {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use self::Misfire::*;
-
-        match *self {
-            InvalidOptions(ref e)  => write!(f, "{}", e),
-            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),
-        }
-    }
-}
-
-static OPTIONS: &'static str = r##"
-DISPLAY OPTIONS
-  -1, --oneline      display one entry per line
-  -G, --grid         display entries in a grid view (default)
-  -l, --long         display extended details and attributes
-  -R, --recurse      recurse into directories
-  -T, --tree         recurse into subdirectories in a tree view
-  -x, --across       sort multi-column view entries across
-  --color, --colour  when to colourise the output
-
-FILTERING AND SORTING OPTIONS
-  -a, --all                  show dot-files
-  -d, --list-dirs            list directories as regular files
-  -r, --reverse              reverse order of files
-  -s, --sort WORD            field to sort by
-  --group-directories-first  list directories before other files
-"##;
-
-static LONG_OPTIONS: &'static str = r##"
-LONG VIEW OPTIONS
-  -b, --binary       use binary prefixes in file sizes
-  -B, --bytes        list file sizes in bytes, without prefixes
-  -g, --group        show group as well as user
-  -h, --header       show a header row at the top
-  -H, --links        show number of hard links
-  -i, --inode        show each file's inode number
-  -L, --level DEPTH  maximum depth of recursion
-  -m, --modified     display timestamp of most recent modification
-  -S, --blocks       show number of file system blocks
-  -t, --time WORD    which timestamp to show for a file
-  -u, --accessed     display timestamp of last access for a file
-  -U, --created      display timestamp of creation for a file
-"##;
-
-static GIT_HELP:      &'static str = r##"  --git              show git status for files"##;
-static EXTENDED_HELP: &'static str = r##"  -@, --extended     display extended attribute keys and sizes"##;
-
-#[cfg(test)]
-mod test {
-    use super::{Options, Misfire, SortField, SortCase};
-    use fs::feature::xattr;
-
-    fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
-        match misfire {
-            Err(Misfire::Help(_)) => true,
-            _                     => false,
-        }
-    }
-
-    #[test]
-    fn help() {
-        let opts = Options::getopts(&[ "--help".to_string() ]);
-        assert!(is_helpful(opts))
-    }
-
-    #[test]
-    fn help_with_file() {
-        let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
-        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() ])
-    }
-
-    #[test]
-    fn no_args() {
-        let args = Options::getopts(&[]).unwrap().1;
-        assert!(args.is_empty());  // Listing the `.` directory is done in main.rs
-    }
-
-    #[test]
-    fn file_sizes() {
-        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
-    }
-
-    #[test]
-    fn just_binary() {
-        let opts = Options::getopts(&[ "--binary".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
-    }
-
-    #[test]
-    fn just_bytes() {
-        let opts = Options::getopts(&[ "--bytes".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
-    }
-
-    #[test]
-    fn long_across() {
-        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
-    }
-
-    #[test]
-    fn oneline_across() {
-        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
-    }
-
-    #[test]
-    fn just_header() {
-        let opts = Options::getopts(&[ "--header".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
-    }
-
-    #[test]
-    fn just_group() {
-        let opts = Options::getopts(&[ "--group".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
-    }
-
-    #[test]
-    fn just_inode() {
-        let opts = Options::getopts(&[ "--inode".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
-    }
-
-    #[test]
-    fn just_links() {
-        let opts = Options::getopts(&[ "--links".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
-    }
-
-    #[test]
-    fn just_blocks() {
-        let opts = Options::getopts(&[ "--blocks".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
-    }
-
-    #[test]
-    fn test_sort_size() {
-        let opts = Options::getopts(&[ "--sort=size".to_string() ]);
-        assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
-    }
-
-    #[test]
-    fn test_sort_name() {
-        let opts = Options::getopts(&[ "--sort=name".to_string() ]);
-        assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
-    }
-
-    #[test]
-    fn test_sort_name_lowercase() {
-        let opts = Options::getopts(&[ "--sort=Name".to_string() ]);
-        assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
-    }
-
-    #[test]
-    #[cfg(feature="git")]
-    fn just_git() {
-        let opts = Options::getopts(&[ "--git".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
-    }
-
-    #[test]
-    fn extended_without_long() {
-        if xattr::ENABLED {
-            let opts = Options::getopts(&[ "--extended".to_string() ]);
-            assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
-        }
-    }
-
-    #[test]
-    fn level_without_recurse_or_tree() {
-        let opts = Options::getopts(&[ "--level".to_string(), "69105".to_string() ]);
-        assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
-    }
-}

+ 107 - 0
src/options/dir_action.rs

@@ -0,0 +1,107 @@
+use getopts;
+
+use options::misfire::Misfire;
+
+
+/// What to do when encountering a directory?
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum DirAction {
+
+    /// This directory should be listed along with the regular files, instead
+    /// of having its contents queried.
+    AsFile,
+
+    /// This directory should not be listed, and should instead be opened and
+    /// *its* files listed separately. This is the default behaviour.
+    List,
+
+    /// This directory should be listed along with the regular files, and then
+    /// its contents should be listed afterward. The recursive contents of
+    /// *those* contents are dictated by the options argument.
+    Recurse(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");
+
+        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 )  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
+            (true,  false, false)  => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
+            (false, true,  _    )  => Ok(DirAction::AsFile),
+            (false, false, _    )  => Ok(DirAction::List),
+        }
+    }
+
+    /// Gets the recurse options, if this dir action has any.
+    pub fn recurse_options(&self) -> Option<RecurseOptions> {
+        match *self {
+            DirAction::Recurse(opts) => Some(opts),
+            _ => None,
+        }
+    }
+
+    /// Whether to treat directories as regular files or not.
+    pub fn treat_dirs_as_files(&self) -> bool {
+        match *self {
+            DirAction::AsFile => true,
+            DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
+            _ => false,
+        }
+    }
+}
+
+
+/// The options that determine how to recurse into a directory.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct RecurseOptions {
+
+    /// Whether recursion should be done as a tree or as multiple individual
+    /// views of files.
+    pub tree: bool,
+
+    /// The maximum number of times that recursion should descend to, if one
+    /// is specified.
+    pub max_depth: Option<usize>,
+}
+
+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() {
+                Ok(l)   => Some(l),
+                Err(e)  => return Err(Misfire::FailedParse(e)),
+            }
+        }
+        else {
+            None
+        };
+
+        Ok(RecurseOptions {
+            tree: tree,
+            max_depth: max_depth,
+        })
+    }
+
+    /// Returns whether a directory of the given depth would be too deep.
+    pub fn is_too_deep(&self, depth: usize) -> bool {
+        match self.max_depth {
+            None    => false,
+            Some(d) => {
+                d <= depth
+            }
+        }
+    }
+}

+ 228 - 0
src/options/filter.rs

@@ -0,0 +1,228 @@
+use std::cmp::Ordering;
+use std::os::unix::fs::MetadataExt;
+
+use getopts;
+use natord;
+
+use fs::File;
+use options::misfire::Misfire;
+
+
+/// The **file filter** processes a vector of files before outputting them,
+/// filtering and sorting the files depending on the user’s command-line
+/// flags.
+#[derive(Default, PartialEq, Debug, Copy, Clone)]
+pub struct FileFilter {
+
+    /// Whether directories should be listed first, and other types of file
+    /// second. Some users prefer it like this.
+    pub list_dirs_first: bool,
+
+    /// The metadata field to sort by.
+    pub sort_field: SortField,
+
+    /// Whether to reverse the sorting order. This would sort the largest
+    /// files first, or files starting with Z, or the most-recently-changed
+    /// ones, depending on the sort field.
+    pub reverse: bool,
+
+    /// Whether to include invisible “dot” files when listing a directory.
+    ///
+    /// Files starting with a single “.” are used to determine “system” or
+    /// “configuration” files that should not be displayed in a regular
+    /// directory listing.
+    ///
+    /// This came about more or less by a complete historical accident,
+    /// when the original `ls` tried to hide `.` and `..`:
+    /// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
+    ///
+    ///   When one typed ls, however, these files appeared, so either Ken or
+    ///   Dennis added a simple test to the program. It was in assembler then,
+    ///   but the code in question was equivalent to something like this:
+    ///      if (name[0] == '.') continue;
+    ///   This statement was a little shorter than what it should have been,
+    ///   which is:
+    ///      if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
+    ///   but hey, it was easy.
+    ///
+    ///   Two things resulted.
+    ///
+    ///   First, a bad precedent was set. A lot of other lazy programmers
+    ///   introduced bugs by making the same simplification. Actual files
+    ///   beginning with periods are often skipped when they should be counted.
+    ///
+    ///   Second, and much worse, the idea of a "hidden" or "dot" file was
+    ///   created. As a consequence, more lazy programmers started dropping
+    ///   files into everyone's home directory. I don't have all that much
+    ///   stuff installed on the machine I'm using to type this, but my home
+    ///   directory has about a hundred dot files and I don't even know what
+    ///   most of them are or whether they're still needed. Every file name
+    ///   evaluation that goes through my home directory is slowed down by
+    ///   this accumulated sludge.
+    show_invisibles: bool,
+}
+
+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> {
+        let sort_field = try!(SortField::deduce(&matches));
+
+        Ok(FileFilter {
+            list_dirs_first: matches.opt_present("group-directories-first"),
+            reverse:         matches.opt_present("reverse"),
+            show_invisibles: matches.opt_present("all"),
+            sort_field:      sort_field,
+        })
+    }
+
+    /// Remove every file in the given vector that does *not* pass the
+    /// filter predicate.
+    pub fn filter_files(&self, files: &mut Vec<File>) {
+        if !self.show_invisibles {
+            files.retain(|f| !f.is_dotfile());
+        }
+    }
+
+    /// Sort the files in the given vector based on the sort field option.
+    pub fn sort_files<'_, F>(&self, files: &mut Vec<F>)
+    where F: AsRef<File<'_>> {
+
+        files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
+
+        if self.reverse {
+            files.reverse();
+        }
+
+        if self.list_dirs_first {
+            // This relies on the fact that `sort_by` is stable.
+            files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
+        }
+    }
+
+    /// Compares two files to determine the order they should be listed in,
+    /// depending on the search field.
+    pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
+        use self::SortCase::{Sensitive, Insensitive};
+
+        match self.sort_field {
+            SortField::Unsorted  => Ordering::Equal,
+
+            SortField::Name(Sensitive)    => natord::compare(&a.name, &b.name),
+            SortField::Name(Insensitive)  => natord::compare_ignore_case(&a.name, &b.name),
+
+            SortField::Size          => a.metadata.len().cmp(&b.metadata.len()),
+            SortField::FileInode     => a.metadata.ino().cmp(&b.metadata.ino()),
+            SortField::ModifiedDate  => a.metadata.mtime().cmp(&b.metadata.mtime()),
+            SortField::AccessedDate  => a.metadata.atime().cmp(&b.metadata.atime()),
+            SortField::CreatedDate   => a.metadata.ctime().cmp(&b.metadata.ctime()),
+
+            SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
+                Ordering::Equal  => natord::compare(&*a.name, &*b.name),
+                order            => order,
+            },
+
+            SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
+                Ordering::Equal  => natord::compare_ignore_case(&*a.name, &*b.name),
+                order            => order,
+            },
+        }
+    }
+}
+
+
+/// User-supplied field to sort by.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum SortField {
+
+    /// Don't apply any sorting. This is usually used as an optimisation in
+    /// scripts, where the order doesn't matter.
+    Unsorted,
+
+    /// The file name. This is the default sorting.
+    Name(SortCase),
+
+    /// The file's extension, with extensionless files being listed first.
+    Extension(SortCase),
+
+    /// The file's size.
+    Size,
+
+    /// The file's inode. This is sometimes analogous to the order in which
+    /// the files were created on the hard drive.
+    FileInode,
+
+    /// The time at which this file was modified (the `mtime`).
+    ///
+    /// As this is stored as a Unix timestamp, rather than a local time
+    /// instance, the time zone does not matter and will only be used to
+    /// display the timestamps, not compare them.
+    ModifiedDate,
+
+    /// The time at this file was accessed (the `atime`).
+    ///
+    /// Oddly enough, this field rarely holds the *actual* accessed time.
+    /// Recording a read time means writing to the file each time it’s read
+    /// slows the whole operation down, so many systems will only update the
+    /// timestamp in certain circumstances. This has become common enough that
+    /// it’s now expected behaviour for the `atime` field.
+    /// http://unix.stackexchange.com/a/8842
+    AccessedDate,
+
+    /// The time at which this file was changed or created (the `ctime`).
+    ///
+    /// Contrary to the name, this field is used to mark the time when a
+    /// file's metadata changed -- its permissions, owners, or link count.
+    ///
+    /// In original Unix, this was, however, meant as creation time.
+    /// https://www.bell-labs.com/usr/dmr/www/cacm.html
+    CreatedDate,
+}
+
+/// Whether a field should be sorted case-sensitively or case-insensitively.
+///
+/// This determines which of the `natord` functions to use.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum SortCase {
+
+    /// Sort files case-sensitively with uppercase first, with ‘A’ coming
+    /// before ‘a’.
+    Sensitive,
+
+    /// Sort files case-insensitively, with ‘A’ being equal to ‘a’.
+    Insensitive,
+}
+
+impl Default for SortField {
+    fn default() -> SortField {
+        SortField::Name(SortCase::Sensitive)
+    }
+}
+
+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> {
+        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),
+                "none"                => Ok(SortField::Unsorted),
+                "inode"               => Ok(SortField::FileInode),
+                field                 => Err(Misfire::bad_argument("sort", field))
+            }
+        }
+        else {
+            Ok(SortField::default())
+        }
+    }
+}

+ 37 - 0
src/options/help.rs

@@ -0,0 +1,37 @@
+
+pub static OPTIONS: &'static str = r##"
+DISPLAY OPTIONS
+  -1, --oneline      display one entry per line
+  -G, --grid         display entries in a grid view (default)
+  -l, --long         display extended details and attributes
+  -R, --recurse      recurse into directories
+  -T, --tree         recurse into subdirectories in a tree view
+  -x, --across       sort multi-column view entries across
+  --color, --colour  when to colourise the output
+
+FILTERING AND SORTING OPTIONS
+  -a, --all                  show dot-files
+  -d, --list-dirs            list directories as regular files
+  -r, --reverse              reverse order of files
+  -s, --sort WORD            field to sort by
+  --group-directories-first  list directories before other files
+"##;
+
+pub static LONG_OPTIONS: &'static str = r##"
+LONG VIEW OPTIONS
+  -b, --binary       use binary prefixes in file sizes
+  -B, --bytes        list file sizes in bytes, without prefixes
+  -g, --group        show group as well as user
+  -h, --header       show a header row at the top
+  -H, --links        show number of hard links
+  -i, --inode        show each file's inode number
+  -L, --level DEPTH  maximum depth of recursion
+  -m, --modified     display timestamp of most recent modification
+  -S, --blocks       show number of file system blocks
+  -t, --time WORD    which timestamp to show for a file
+  -u, --accessed     display timestamp of last access for a file
+  -U, --created      display timestamp of creation for a file
+"##;
+
+pub static GIT_HELP:      &'static str = r##"  --git              show git status for files"##;
+pub static EXTENDED_HELP: &'static str = r##"  -@, --extended     display extended attribute keys and sizes"##;

+ 69 - 0
src/options/misfire.rs

@@ -0,0 +1,69 @@
+use std::fmt;
+use std::num::ParseIntError;
+
+use getopts;
+
+
+/// A **misfire** is a thing that can happen instead of listing files -- a
+/// catch-all for anything outside the program’s normal execution.
+#[derive(PartialEq, Debug)]
+pub enum Misfire {
+
+    /// The getopts crate didn’t like these arguments.
+    InvalidOptions(getopts::Fail),
+
+    /// The user asked for help. This isn’t strictly an error, which is why
+    /// this enum isn’t named Error!
+    Help(String),
+
+    /// The user wanted the version number.
+    Version,
+
+    /// Two options were given that conflict with one another.
+    Conflict(&'static str, &'static str),
+
+    /// An option was given that does nothing when another one either is or
+    /// isn't present.
+    Useless(&'static str, bool, &'static str),
+
+    /// An option was given that does nothing when either of two other options
+    /// are not present.
+    Useless2(&'static str, &'static str, &'static str),
+
+    /// A numeric option was given that failed to be parsed as a number.
+    FailedParse(ParseIntError),
+}
+
+impl Misfire {
+
+    /// The OS return code this misfire should signify.
+    pub fn error_code(&self) -> i32 {
+        if let Misfire::Help(_) = *self { 2 }
+                                   else { 3 }
+    }
+
+    /// The Misfire that happens when an option gets given the wrong
+    /// 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) -> Misfire {
+        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--{} {}", option, otherwise)))
+    }
+}
+
+impl fmt::Display for Misfire {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::Misfire::*;
+
+        match *self {
+            InvalidOptions(ref e)  => write!(f, "{}", e),
+            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),
+        }
+    }
+}

+ 282 - 0
src/options/mod.rs

@@ -0,0 +1,282 @@
+use getopts;
+
+use fs::feature::xattr;
+use output::{Details, GridDetails};
+
+mod dir_action;
+pub use self::dir_action::{DirAction, RecurseOptions};
+
+mod filter;
+pub use self::filter::{FileFilter, SortField, SortCase};
+
+mod help;
+use self::help::*;
+
+mod misfire;
+pub use self::misfire::Misfire;
+
+mod view;
+pub use self::view::View;
+
+
+/// These **options** represent a parsed, error-checked versions of the
+/// user’s command-line options.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct Options {
+
+    /// The action to perform when encountering a directory rather than a
+    /// regular file.
+    pub dir_action: DirAction,
+
+    /// How to sort and filter files before outputting them.
+    pub filter: FileFilter,
+
+    /// The type of output to use (lines, grid, or details).
+    pub view: View,
+}
+
+impl Options {
+
+    /// Call getopts on the given slice of command-line strings.
+    #[allow(unused_results)]
+    pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
+        let mut opts = getopts::Options::new();
+
+        opts.optflag("v", "version",   "display 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("G", "grid",      "display entries in a grid view (default)");
+        opts.optflag("l", "long",      "display extended details and attributes");
+        opts.optflag("R", "recurse",   "recurse into directories");
+        opts.optflag("T", "tree",      "recurse into subdirectories in a tree view");
+        opts.optflag("x", "across",    "sort multi-column view entries across");
+        opts.optopt ("",  "color",     "when to show anything in colours", "WHEN");
+        opts.optopt ("",  "colour",    "when to show anything in colours (alternate spelling)", "WHEN");
+
+        // Filtering and sorting options
+        opts.optflag("",  "group-directories-first", "list directories before other files");
+        opts.optflag("a", "all",       "show dot-files");
+        opts.optflag("d", "list-dirs", "list directories as regular files");
+        opts.optflag("r", "reverse",   "reverse order of files");
+        opts.optopt ("s", "sort",      "field to sort by", "WORD");
+
+        // Long view options
+        opts.optflag("b", "binary",    "use binary prefixes in file sizes");
+        opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
+        opts.optflag("g", "group",     "show group as well as user");
+        opts.optflag("h", "header",    "show a header row at the top");
+        opts.optflag("H", "links",     "show number of hard links");
+        opts.optflag("i", "inode",     "show each file's inode number");
+        opts.optopt ("L", "level",     "maximum depth of recursion", "DEPTH");
+        opts.optflag("m", "modified",  "display timestamp of most recent modification");
+        opts.optflag("S", "blocks",    "show number of file system blocks");
+        opts.optopt ("t", "time",      "which timestamp to show for a file", "WORD");
+        opts.optflag("u", "accessed",  "display timestamp of last access for a file");
+        opts.optflag("U", "created",   "display timestamp of creation for a file");
+
+        if cfg!(feature="git") {
+            opts.optflag("", "git", "show git status");
+        }
+
+        if xattr::ENABLED {
+            opts.optflag("@", "extended", "display extended attribute keys and sizes");
+        }
+
+        let matches = match opts.parse(args) {
+            Ok(m)   => m,
+            Err(e)  => return Err(Misfire::InvalidOptions(e)),
+        };
+
+        if matches.opt_present("help") {
+            let mut help_string = "Usage:\n  exa [options] [files...]\n".to_owned();
+
+            if !matches.opt_present("long") {
+                help_string.push_str(OPTIONS);
+            }
+
+            help_string.push_str(LONG_OPTIONS);
+
+            if cfg!(feature="git") {
+                help_string.push_str(GIT_HELP);
+                help_string.push('\n');
+            }
+
+            if xattr::ENABLED {
+                help_string.push_str(EXTENDED_HELP);
+                help_string.push('\n');
+            }
+
+            return Err(Misfire::Help(help_string));
+        }
+        else if matches.opt_present("version") {
+            return Err(Misfire::Version);
+        }
+
+        let options = try!(Options::deduce(&matches));
+        Ok((options, matches.free))
+    }
+
+    /// Whether the View specified in this set of options includes a Git
+    /// status column. It’s only worth trying to discover a repository if the
+    /// results will end up being displayed.
+    pub fn should_scan_for_git(&self) -> bool {
+        match self.view {
+            View::Details(Details { columns: Some(cols), .. }) => cols.should_scan_for_git(),
+            View::GridDetails(GridDetails { details: Details { columns: Some(cols), .. }, .. }) => cols.should_scan_for_git(),
+            _ => false,
+        }
+    }
+
+    /// 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> {
+        let dir_action = try!(DirAction::deduce(&matches));
+        let filter = try!(FileFilter::deduce(&matches));
+        let view = try!(View::deduce(&matches, filter, dir_action));
+
+        Ok(Options {
+            dir_action: dir_action,
+            view:       view,
+            filter:     filter,
+        })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::{Options, Misfire, SortField, SortCase};
+    use fs::feature::xattr;
+
+    fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
+        match misfire {
+            Err(Misfire::Help(_)) => true,
+            _                     => false,
+        }
+    }
+
+    #[test]
+    fn help() {
+        let opts = Options::getopts(&[ "--help".to_string() ]);
+        assert!(is_helpful(opts))
+    }
+
+    #[test]
+    fn help_with_file() {
+        let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
+        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() ])
+    }
+
+    #[test]
+    fn no_args() {
+        let args = Options::getopts(&[]).unwrap().1;
+        assert!(args.is_empty());  // Listing the `.` directory is done in main.rs
+    }
+
+    #[test]
+    fn file_sizes() {
+        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
+    }
+
+    #[test]
+    fn just_binary() {
+        let opts = Options::getopts(&[ "--binary".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
+    }
+
+    #[test]
+    fn just_bytes() {
+        let opts = Options::getopts(&[ "--bytes".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
+    }
+
+    #[test]
+    fn long_across() {
+        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
+        assert_eq!(opts, Err(Misfire::Useless("across", true, "long")))
+    }
+
+    #[test]
+    fn oneline_across() {
+        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
+        assert_eq!(opts, Err(Misfire::Useless("across", true, "oneline")))
+    }
+
+    #[test]
+    fn just_header() {
+        let opts = Options::getopts(&[ "--header".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
+    }
+
+    #[test]
+    fn just_group() {
+        let opts = Options::getopts(&[ "--group".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
+    }
+
+    #[test]
+    fn just_inode() {
+        let opts = Options::getopts(&[ "--inode".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
+    }
+
+    #[test]
+    fn just_links() {
+        let opts = Options::getopts(&[ "--links".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
+    }
+
+    #[test]
+    fn just_blocks() {
+        let opts = Options::getopts(&[ "--blocks".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
+    }
+
+    #[test]
+    fn test_sort_size() {
+        let opts = Options::getopts(&[ "--sort=size".to_string() ]);
+        assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
+    }
+
+    #[test]
+    fn test_sort_name() {
+        let opts = Options::getopts(&[ "--sort=name".to_string() ]);
+        assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
+    }
+
+    #[test]
+    fn test_sort_name_lowercase() {
+        let opts = Options::getopts(&[ "--sort=Name".to_string() ]);
+        assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
+    }
+
+    #[test]
+    #[cfg(feature="git")]
+    fn just_git() {
+        let opts = Options::getopts(&[ "--git".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
+    }
+
+    #[test]
+    fn extended_without_long() {
+        if xattr::ENABLED {
+            let opts = Options::getopts(&[ "--extended".to_string() ]);
+            assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
+        }
+    }
+
+    #[test]
+    fn level_without_recurse_or_tree() {
+        let opts = Options::getopts(&[ "--level".to_string(), "69105".to_string() ]);
+        assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
+    }
+}

+ 355 - 0
src/options/view.rs

@@ -0,0 +1,355 @@
+use std::env::var_os;
+
+use getopts;
+
+use output::Colours;
+use output::{Grid, Details, GridDetails, Lines};
+use options::{FileFilter, DirAction, Misfire};
+use output::column::{Columns, TimeTypes, SizeFormat};
+use term::dimensions;
+use fs::feature::xattr;
+
+
+/// The **view** contains all information about how to format output.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum View {
+    Details(Details),
+    Grid(Grid),
+    GridDetails(GridDetails),
+    Lines(Lines),
+}
+
+impl View {
+
+    /// Determine which view to use and all of that view’s arguments.
+    pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
+        use options::misfire::Misfire::*;
+
+        let long = || {
+            if matches.opt_present("across") && !matches.opt_present("grid") {
+                Err(Useless("across", true, "long"))
+            }
+            else if matches.opt_present("oneline") {
+                Err(Useless("oneline", true, "long"))
+            }
+            else {
+                let term_colours = try!(TerminalColours::deduce(matches));
+                let colours = match term_colours {
+                    TerminalColours::Always    => Colours::colourful(),
+                    TerminalColours::Never     => Colours::plain(),
+                    TerminalColours::Automatic => {
+                        if dimensions().is_some() {
+                            Colours::colourful()
+                        }
+                        else {
+                            Colours::plain()
+                        }
+                    },
+                };
+
+                let details = Details {
+                    columns: Some(try!(Columns::deduce(matches))),
+                    header: matches.opt_present("header"),
+                    recurse: dir_action.recurse_options(),
+                    filter: filter,
+                    xattr: xattr::ENABLED && matches.opt_present("extended"),
+                    colours: colours,
+                };
+
+                Ok(details)
+            }
+        };
+
+        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"));
+                }
+            }
+
+            if cfg!(feature="git") && matches.opt_present("git") {
+                Err(Useless("git", false, "long"))
+            }
+            else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
+                Err(Useless2("level", "recurse", "tree"))
+            }
+            else if xattr::ENABLED && matches.opt_present("extended") {
+                Err(Useless("extended", false, "long"))
+            }
+            else {
+                Ok(())
+            }
+        };
+
+        let other_options_scan = || {
+            let term_colours = try!(TerminalColours::deduce(matches));
+            let term_width   = try!(TerminalWidth::deduce(matches));
+
+            if let Some(&width) = term_width.as_ref() {
+                let colours = match term_colours {
+                    TerminalColours::Always    => Colours::colourful(),
+                    TerminalColours::Never     => Colours::plain(),
+                    TerminalColours::Automatic => Colours::colourful(),
+                };
+
+                if matches.opt_present("oneline") {
+                    if matches.opt_present("across") {
+                        Err(Useless("across", true, "oneline"))
+                    }
+                    else {
+                        let lines = Lines {
+                             colours: colours,
+                        };
+
+                        Ok(View::Lines(lines))
+                    }
+                }
+                else if matches.opt_present("tree") {
+                    let details = Details {
+                        columns: None,
+                        header: false,
+                        recurse: dir_action.recurse_options(),
+                        filter: filter,
+                        xattr: false,
+                        colours: colours,
+                    };
+
+                    Ok(View::Details(details))
+                }
+                else {
+                    let grid = Grid {
+                        across: matches.opt_present("across"),
+                        console_width: width,
+                        colours: colours,
+                    };
+
+                    Ok(View::Grid(grid))
+                }
+            }
+            else {
+                // If the terminal width couldn’t be matched for some reason, such
+                // as the program’s stdout being connected to a file, then
+                // fallback to the lines view.
+
+                let colours = match term_colours {
+                    TerminalColours::Always    => Colours::colourful(),
+                    TerminalColours::Never     => Colours::plain(),
+                    TerminalColours::Automatic => Colours::plain(),
+                };
+
+                if matches.opt_present("tree") {
+                    let details = Details {
+                        columns: None,
+                        header: false,
+                        recurse: dir_action.recurse_options(),
+                        filter: filter,
+                        xattr: false,
+                        colours: colours,
+                    };
+
+                    Ok(View::Details(details))
+                }
+                else {
+                    let lines = Lines {
+                         colours: colours,
+                    };
+
+                    Ok(View::Lines(lines))
+                }
+            }
+        };
+
+        if matches.opt_present("long") {
+            let long_options = try!(long());
+
+            if matches.opt_present("grid") {
+                match other_options_scan() {
+                    Ok(View::Grid(grid)) => return Ok(View::GridDetails(GridDetails { grid: grid, details: long_options })),
+                    Ok(lines)            => return Ok(lines),
+                    Err(e)               => return Err(e),
+                };
+            }
+            else {
+                return Ok(View::Details(long_options));
+            }
+        }
+
+        try!(long_options_scan());
+
+        other_options_scan()
+    }
+}
+
+
+/// The width of the terminal requested by the user.
+#[derive(PartialEq, Debug)]
+enum TerminalWidth {
+
+    /// The user requested this specific number of columns.
+    Set(usize),
+
+    /// The terminal was found to have this number of columns.
+    Terminal(usize),
+
+    /// The user didn’t request any particular terminal width.
+    Unset,
+}
+
+impl TerminalWidth {
+
+    /// Determine a requested terminal width from the command-line arguments.
+    ///
+    /// Returns an error if a requested width doesn’t parse to an integer.
+    fn deduce(_: &getopts::Matches) -> Result<TerminalWidth, Misfire> {
+        if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
+            match columns.parse() {
+                Ok(width)  => Ok(TerminalWidth::Set(width)),
+                Err(e)     => Err(Misfire::FailedParse(e)),
+            }
+        }
+        else if let Some((width, _)) = dimensions() {
+            Ok(TerminalWidth::Terminal(width))
+        }
+        else {
+            Ok(TerminalWidth::Unset)
+        }
+    }
+
+    fn as_ref(&self) -> Option<&usize> {
+        match *self {
+            TerminalWidth::Set(ref width)       => Some(width),
+            TerminalWidth::Terminal(ref width)  => Some(width),
+            TerminalWidth::Unset                => None,
+        }
+    }
+}
+
+
+impl Columns {
+    fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
+        Ok(Columns {
+            size_format: try!(SizeFormat::deduce(matches)),
+            time_types:  try!(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"),
+        })
+    }
+}
+
+
+impl SizeFormat {
+
+    /// Determine which file size to use in the file size column based on
+    /// the user’s options.
+    ///
+    /// The default mode is to use the decimal prefixes, as they are the
+    /// most commonly-understood, and don’t involve trying to parse large
+    /// 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");
+
+        match (binary, bytes) {
+            (true,  true )  => Err(Misfire::Conflict("binary", "bytes")),
+            (true,  false)  => Ok(SizeFormat::BinaryBytes),
+            (false, true )  => Ok(SizeFormat::JustBytes),
+            (false, false)  => Ok(SizeFormat::DecimalBytes),
+        }
+    }
+}
+
+
+impl TimeTypes {
+
+    /// Determine which of a file’s time fields should be displayed for it
+    /// based on the user’s options.
+    ///
+    /// There are two separate ways to pick which fields to show: with a
+    /// flag (such as `--modified`) or with a parameter (such as
+    /// `--time=modified`). An error is signaled if both ways are used.
+    ///
+    /// 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");
+
+        if let Some(word) = possible_word {
+            if modified {
+                return Err(Misfire::Useless("modified", true, "time"));
+            }
+            else if created {
+                return Err(Misfire::Useless("created", true, "time"));
+            }
+            else if accessed {
+                return Err(Misfire::Useless("accessed", true, "time"));
+            }
+
+            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)),
+            }
+        }
+        else if modified || created || accessed {
+            Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
+        }
+        else {
+            Ok(TimeTypes::default())
+        }
+    }
+}
+
+
+/// Under what circumstances we should display coloured, rather than plain,
+/// output to the terminal.
+///
+/// By default, we want to display the colours when stdout can display them.
+/// Turning them on when output is going to, say, a pipe, would make programs
+/// such as `grep` or `more` not work properly. So the `Automatic` mode does
+/// this check and only displays colours when they can be truly appreciated.
+#[derive(PartialEq, Debug)]
+enum TerminalColours {
+
+    /// Display them even when output isn’t going to a terminal.
+    Always,
+
+    /// Display them when output is going to a terminal, but not otherwise.
+    Automatic,
+
+    /// Never display them, even when output is going to a terminal.
+    Never,
+}
+
+impl Default for TerminalColours {
+    fn default() -> TerminalColours {
+        TerminalColours::Automatic
+    }
+}
+
+impl TerminalColours {
+
+    /// Determine which terminal colour conditions to use.
+    fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
+        if let Some(word) = matches.opt_str("color").or(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))
+            }
+        }
+        else {
+            Ok(TerminalColours::default())
+        }
+    }
+}