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.
This commit is contained in:
Benjamin Sago 2017-07-26 17:48:18 +01:00
parent 5b1966d261
commit 2d1f462bfa
11 changed files with 429 additions and 307 deletions

4
Vagrantfile vendored
View File

@ -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

View File

@ -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),

View File

@ -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 isnt 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 {

View File

@ -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)),
}

View File

@ -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 users
/// 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 users
/// 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
src/options/flags.rs Normal file
View File

@ -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,
]);

View File

@ -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 didnt like these arguments.
InvalidOptions(getopts::Fail),
/// The getopts crate didnt 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 isnt strictly an error, which is why
/// this enum isnt 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 cant 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--its 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),
}
}
}

View File

@ -69,9 +69,7 @@
//! its 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();
pub fn getopts<'args, I>(args: I) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString> {
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");
}
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 theyve 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);
}
}

View File

@ -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 },
}
// Its 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 its 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' }));

View File

@ -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 views 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 programs 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 {
/// Its 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 }
}
}

View File

@ -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