From 2d1f462bfa3de157d0b194aa96f64c8fa19a5fcc Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 26 Jul 2017 17:48:18 +0100 Subject: [PATCH] Switch to the new options parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Vagrantfile | 4 +- src/bin/main.rs | 5 +- src/exa.rs | 23 +++-- src/options/dir_action.rs | 23 +++-- src/options/filter.rs | 90 +++++++++++------- src/options/flags.rs | 64 +++++++++++++ src/options/misfire.rs | 49 +++++----- src/options/mod.rs | 187 ++++++++++++++++++-------------------- src/options/parser.rs | 110 +++++++++++++--------- src/options/view.rs | 179 ++++++++++++++++++++---------------- xtests/run.sh | 2 +- 11 files changed, 429 insertions(+), 307 deletions(-) create mode 100644 src/options/flags.rs diff --git a/Vagrantfile b/Vagrantfile index 999ed95f..4c26c901 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -59,8 +59,8 @@ Vagrant.configure(2) do |config| config.vm.provision :shell, privileged: true, inline: <<-EOF set -xe - echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \\$*" > /usr/bin/exa - echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \\$*" > /usr/bin/rexa + echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \"\\$*\"" > /usr/bin/exa + echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \"\\$*\"" > /usr/bin/rexa chmod +x /usr/bin/{exa,rexa} EOF diff --git a/src/bin/main.rs b/src/bin/main.rs index 146e1147..774ece1c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,14 +1,15 @@ extern crate exa; use exa::Exa; +use std::ffi::OsString; use std::env::args_os; use std::io::{stdout, stderr, Write, ErrorKind}; use std::process::exit; fn main() { - let args = args_os().skip(1); - match Exa::new(args, &mut stdout()) { + let args: Vec = args_os().skip(1).collect(); + match Exa::new(args.iter(), &mut stdout()) { Ok(mut exa) => { match exa.run() { Ok(exit_status) => exit(exit_status), diff --git a/src/exa.rs b/src/exa.rs index 4e22c8ac..2eef37ad 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -3,7 +3,6 @@ extern crate ansi_term; extern crate datetime; -extern crate getopts; extern crate glob; extern crate libc; extern crate locale; @@ -22,7 +21,7 @@ extern crate zoneinfo_compiled; extern crate lazy_static; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::io::{stderr, Write, Result as IOResult}; use std::path::{Component, PathBuf}; @@ -41,7 +40,7 @@ mod term; /// The main program wrapper. -pub struct Exa<'w, W: Write + 'w> { +pub struct Exa<'args, 'w, W: Write + 'w> { /// List of command-line options, having been successfully parsed. pub options: Options, @@ -53,12 +52,12 @@ pub struct Exa<'w, W: Write + 'w> { /// List of the free command-line arguments that should correspond to file /// names (anything that isn’t an option). - pub args: Vec, + pub args: Vec<&'args OsStr>, } -impl<'w, W: Write + 'w> Exa<'w, W> { - pub fn new(args: C, writer: &'w mut W) -> Result, Misfire> - where C: IntoIterator, C::Item: AsRef { +impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { + pub fn new(args: I, writer: &'w mut W) -> Result, Misfire> + where I: Iterator { 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 { diff --git a/src/options/dir_action.rs b/src/options/dir_action.rs index 022ac08a..24387ded 100644 --- a/src/options/dir_action.rs +++ b/src/options/dir_action.rs @@ -1,24 +1,23 @@ -use getopts; +use options::parser::Matches; +use options::{flags, Misfire}; -use options::misfire::Misfire; use fs::dir_action::{DirAction, RecurseOptions}; - impl DirAction { /// Determine which action to perform when trying to list a directory. - pub fn deduce(matches: &getopts::Matches) -> Result { - 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 { + 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 { - let max_depth = if let Some(level) = matches.opt_str("level") { - match level.parse() { + pub fn deduce(matches: &Matches, tree: bool) -> Result { + 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)), } diff --git a/src/options/filter.rs b/src/options/filter.rs index 2ffd50e6..1f77ab45 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -1,19 +1,20 @@ -use getopts; use glob; use fs::DotFilter; use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns}; -use options::misfire::Misfire; + +use options::{flags, Misfire}; +use options::parser::Matches; impl FileFilter { /// Determines the set of file filter options to use, based on the user’s /// command-line arguments. - pub fn deduce(matches: &getopts::Matches) -> Result { + pub fn deduce(matches: &Matches) -> Result { 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 { + fn deduce(matches: &Matches) -> Result { 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 { - let dots = match matches.opt_count("all") { + pub fn deduce(matches: &Matches) -> Result { + let dots = match matches.count(&flags::ALL) { 0 => return Ok(DotFilter::JustFiles), 1 => DotFilter::Dotfiles, _ => DotFilter::DotfilesAndDots, }; - if matches.opt_present("tree") { - Err(Misfire::Useless("all --all", true, "tree")) + if matches.has(&flags::TREE) { + Err(Misfire::TreeAllAll) } else { Ok(dots) @@ -85,14 +108,15 @@ impl IgnorePatterns { /// Determines the set of file filter options to use, based on the user’s /// command-line arguments. - pub fn deduce(matches: &getopts::Matches) -> Result { - let patterns = match matches.opt_str("ignore-glob") { + pub fn deduce(matches: &Matches) -> Result { + 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 }) } } diff --git a/src/options/flags.rs b/src/options/flags.rs new file mode 100644 index 00000000..d0cf319a --- /dev/null +++ b/src/options/flags.rs @@ -0,0 +1,64 @@ +use options::parser::{Arg, Args, TakesValue}; + + +// exa options +pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value: TakesValue::Forbidden }; +pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden }; + +// display options +pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden }; +pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }; +pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden }; +pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden }; +pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden }; +pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden }; +pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; + +pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary }; +pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary }; + +pub static COLOR_SCALE: Arg = Arg { short: None, long: "color-scale", takes_value: TakesValue::Forbidden }; +pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden }; + +// filtering and sorting options +pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden }; +pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden }; +pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Forbidden }; +pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden }; +pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary }; +pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary }; +pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden }; + +// display options +pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden }; +pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden }; +pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden }; +pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden }; +pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; +pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; +pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; +pub static BLOCKS: Arg = Arg { short: Some(b'S'), long: "blocks", takes_value: TakesValue::Forbidden }; +pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Forbidden }; +pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden }; +pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden }; +pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary }; + +// optional feature options +pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden }; +pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden }; + + +pub static ALL_ARGS: Args = Args(&[ + &VERSION, &HELP, + + &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, + &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, + + &ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &IGNORE_GLOB, &DIRS_FIRST, + + &BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &BLOCKS, + &TIME, &ACCESSED, &CREATED, &TIME_STYLE, + + &GIT, &EXTENDED, +]); + diff --git a/src/options/misfire.rs b/src/options/misfire.rs index 84ea3f57..d34924bc 100644 --- a/src/options/misfire.rs +++ b/src/options/misfire.rs @@ -1,10 +1,11 @@ +use std::ffi::{OsStr, OsString}; use std::fmt; use std::num::ParseIntError; -use getopts; use glob; use options::help::HelpString; +use options::parser::{Arg, ParseError}; /// A list of legal choices for an argument-taking option @@ -13,7 +14,7 @@ pub struct Choices(&'static [&'static str]); impl fmt::Display for Choices { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "(choices: {})", self.0.join(" ")) + write!(f, "(choices: {})", self.0.join(", ")) } } @@ -22,11 +23,11 @@ impl fmt::Display for Choices { #[derive(PartialEq, Debug)] pub enum Misfire { - /// The getopts crate didn’t like these arguments. - InvalidOptions(getopts::Fail), + /// The getopts crate didn’t like these Arguments. + InvalidOptions(ParseError), - /// The user supplied an illegal choice to an argument - BadArgument(getopts::Fail, Choices), + /// The user supplied an illegal choice to an Argument. + BadArgument(&'static Arg, OsString, Choices), /// The user asked for help. This isn’t strictly an error, which is why /// this enum isn’t named Error! @@ -36,15 +37,18 @@ pub enum Misfire { Version, /// Two options were given that conflict with one another. - Conflict(&'static str, &'static str), + Conflict(&'static Arg, &'static Arg), /// An option was given that does nothing when another one either is or /// isn't present. - Useless(&'static str, bool, &'static str), + Useless(&'static Arg, bool, &'static Arg), /// An option was given that does nothing when either of two other options /// are not present. - Useless2(&'static str, &'static str, &'static str), + Useless2(&'static Arg, &'static Arg, &'static Arg), + + /// A very specific edge case where --tree can’t be used with --all twice. + TreeAllAll, /// A numeric option was given that failed to be parsed as a number. FailedParse(ParseIntError), @@ -68,10 +72,8 @@ impl Misfire { /// argument. This has to use one of the `getopts` failure /// variants--it’s meant to take just an option name, rather than an /// option *and* an argument, but it works just as well. - pub fn bad_argument(option: &str, otherwise: &str, legal: &'static [&'static str]) -> Misfire { - Misfire::BadArgument(getopts::Fail::UnrecognizedOption(format!( - "--{} {}", - option, otherwise)), Choices(legal)) + pub fn bad_argument(option: &'static Arg, otherwise: &OsStr, legal: &'static [&'static str]) -> Misfire { + Misfire::BadArgument(option, otherwise.to_os_string(), Choices(legal)) } } @@ -86,16 +88,17 @@ impl fmt::Display for Misfire { use self::Misfire::*; match *self { - InvalidOptions(ref e) => write!(f, "{}", e), - BadArgument(ref e, ref c) => write!(f, "{} {}", e, c), - Help(ref text) => write!(f, "{}", text), - Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")), - Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b), - Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b), - Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b), - Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2), - FailedParse(ref e) => write!(f, "Failed to parse number: {}", e), - FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e), + BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no value {:?} (Choices: {})", a, b, c), + InvalidOptions(ref e) => write!(f, "{:?}", e), + Help(ref text) => write!(f, "{}", text), + Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")), + Conflict(ref a, ref b) => write!(f, "Option {} conflicts with option {}.", a, b), + Useless(ref a, false, ref b) => write!(f, "Option {} is useless without option {}.", a, b), + Useless(ref a, true, ref b) => write!(f, "Option {} is useless given option {}.", a, b), + Useless2(ref a, ref b1, ref b2) => write!(f, "Option {} is useless without options {} or {}.", a, b1, b2), + TreeAllAll => write!(f, "Option --tree is useless given --all --all."), + FailedParse(ref e) => write!(f, "Failed to parse number: {}", e), + FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e), } } } diff --git a/src/options/mod.rs b/src/options/mod.rs index b30e2e95..b45e9108 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -69,9 +69,7 @@ //! it’s clear what the user wants. -use std::ffi::OsStr; - -use getopts; +use std::ffi::{OsStr, OsString}; use fs::feature::xattr; use fs::dir_action::DirAction; @@ -90,6 +88,8 @@ mod misfire; pub use self::misfire::Misfire; mod parser; +mod flags; +use self::parser::Matches; /// These **options** represent a parsed, error-checked versions of the @@ -118,77 +118,29 @@ impl Options { /// Call getopts on the given slice of command-line strings. #[allow(unused_results)] - pub fn getopts(args: C) -> Result<(Options, Vec), Misfire> - where C: IntoIterator, C::Item: AsRef { - let mut opts = getopts::Options::new(); + pub fn getopts<'args, I>(args: I) -> Result<(Options, Vec<&'args OsStr>), Misfire> + where I: IntoIterator { - 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 they’ve been parsed. - fn deduce(matches: &getopts::Matches) -> Result { + fn deduce(matches: &Matches) -> Result { 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 = Vec::new(); - let args = Options::getopts(¬hing).unwrap().1; - assert!(args.is_empty()); // Listing the `.` directory is done in main.rs + let nothing: Vec = Vec::new(); + let outs = Options::getopts(¬hing).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 = Vec::new(); + let nothing: Vec = Vec::new(); let dots = Options::getopts(¬hing).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); } } diff --git a/src/options/parser.rs b/src/options/parser.rs index fadbcb80..249664bd 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -31,6 +31,7 @@ #![allow(unused_variables, dead_code)] use std::ffi::{OsStr, OsString}; +use std::fmt; pub type ShortArg = u8; @@ -66,63 +67,87 @@ pub enum TakesValue { #[derive(PartialEq, Debug)] pub struct Arg { - short: Option, - long: LongArg, - takes_value: TakesValue, + pub short: Option, + pub long: LongArg, + pub takes_value: TakesValue, +} + +impl fmt::Display for Arg { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "--{}", self.long)?; + + if let Some(short) = self.short { + write!(f, " (-{})", short as char)?; + } + + Ok(()) + } } #[derive(PartialEq, Debug)] -pub struct Args(&'static [&'static Arg]); +pub struct Args(pub &'static [&'static Arg]); impl Args { - fn lookup_short<'a>(&self, short: ShortArg) -> Result<&Arg, ParseError<'a>> { + fn lookup_short<'a>(&self, short: ShortArg) -> Result<&Arg, ParseError> { match self.0.into_iter().find(|arg| arg.short == Some(short)) { Some(arg) => Ok(arg), None => Err(ParseError::UnknownShortArgument { attempt: short }) } } - fn lookup_long<'a>(&self, long: &'a OsStr) -> Result<&Arg, ParseError<'a>> { + fn lookup_long<'a>(&self, long: &'a OsStr) -> Result<&Arg, ParseError> { match self.0.into_iter().find(|arg| arg.long == long) { Some(arg) => Ok(arg), - None => Err(ParseError::UnknownArgument { attempt: long }) + None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() }) } } } #[derive(PartialEq, Debug)] -pub struct Matches<'a> { +pub struct Matches<'args> { /// Long and short arguments need to be kept in the same vector, because /// we usually want the one nearest the end to count. - flags: Vec<(Flag, Option<&'a OsStr>)>, - frees: Vec<&'a OsStr>, + pub flags: Vec<(Flag, Option<&'args OsStr>)>, + pub frees: Vec<&'args OsStr>, } impl<'a> Matches<'a> { - fn has(&self, arg: &Arg) -> bool { + pub fn has(&self, arg: &Arg) -> bool { self.flags.iter().rev() .find(|tuple| tuple.1.is_none() && tuple.0.matches(arg)) .is_some() } - fn get(&self, arg: &Arg) -> Option<&OsStr> { + pub fn get(&self, arg: &Arg) -> Option<&OsStr> { self.flags.iter().rev() .find(|tuple| tuple.1.is_some() && tuple.0.matches(arg)) .map(|tuple| tuple.1.unwrap()) } + + pub fn count(&self, arg: &Arg) -> usize { + self.flags.iter() + .filter(|tuple| tuple.0.matches(arg)) + .count() + } } #[derive(PartialEq, Debug)] -pub enum ParseError<'a> { +pub enum ParseError { NeedsValue { flag: Flag }, ForbiddenValue { flag: Flag }, UnknownShortArgument { attempt: ShortArg }, - UnknownArgument { attempt: &'a OsStr }, + UnknownArgument { attempt: OsString }, } +// It’s technically possible for ParseError::UnknownArgument to borrow its +// OsStr rather than owning it, but that would give ParseError a lifetime, +// which would give Misfire a lifetime, which gets used everywhere. And this +// only happens when an error occurs, so it’s not really worth it. -fn parse<'a>(args: Args, inputs: &'a [OsString]) -> Result, ParseError<'a>> { + +pub fn parse<'args, I>(args: &Args, inputs: I) -> Result, ParseError> +where I: IntoIterator { use std::os::unix::ffi::OsStrExt; use self::TakesValue::*; @@ -133,8 +158,8 @@ fn parse<'a>(args: Args, inputs: &'a [OsString]) -> Result, 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, 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, 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' })); diff --git a/src/options/view.rs b/src/options/view.rs index 012fade7..e164c939 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -1,21 +1,22 @@ use std::env::var_os; -use getopts; - -use info::filetype::FileExtensions; use output::Colours; use output::{View, Mode, grid, details}; use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions}; use output::file_name::{Classify, FileStyle}; use output::time::TimeFormat; -use options::Misfire; + +use options::{flags, Misfire}; +use options::parser::Matches; + use fs::feature::xattr; +use info::filetype::FileExtensions; impl View { /// Determine which view to use and all of that view’s arguments. - pub fn deduce(matches: &getopts::Matches) -> Result { + pub fn deduce(matches: &Matches) -> Result { 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 { + pub fn deduce(matches: &Matches) -> Result { use options::misfire::Misfire::*; let long = || { - if matches.opt_present("across") && !matches.opt_present("grid") { - Err(Useless("across", true, "long")) + if matches.has(&flags::ACROSS) && !matches.has(&flags::GRID) { + Err(Useless(&flags::ACROSS, true, &flags::LONG)) } - else if matches.opt_present("oneline") { - Err(Useless("oneline", true, "long")) + else if matches.has(&flags::ONE_LINE) { + Err(Useless(&flags::ONE_LINE, true, &flags::LONG)) } else { Ok(details::Options { table: Some(TableOptions::deduce(matches)?), - header: matches.opt_present("header"), - xattr: xattr::ENABLED && matches.opt_present("extended"), + header: matches.has(&flags::HEADER), + xattr: xattr::ENABLED && matches.has(&flags::EXTENDED), }) } }; let long_options_scan = || { - for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] { - if matches.opt_present(option) { - return Err(Useless(option, false, "long")); + for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS, + &flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] { + if matches.has(option) { + return Err(Useless(*option, false, &flags::LONG)); } } - if cfg!(feature="git") && matches.opt_present("git") { - Err(Useless("git", false, "long")) + if cfg!(feature="git") && matches.has(&flags::GIT) { + Err(Useless(&flags::GIT, false, &flags::LONG)) } - else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") { - Err(Useless2("level", "recurse", "tree")) + else if matches.has(&flags::LEVEL) && !matches.has(&flags::RECURSE) && !matches.has(&flags::TREE) { + Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)) } - else if xattr::ENABLED && matches.opt_present("extended") { - Err(Useless("extended", false, "long")) + else if xattr::ENABLED && matches.has(&flags::EXTENDED) { + Err(Useless(&flags::EXTENDED, false, &flags::LONG)) } else { Ok(()) @@ -69,15 +71,15 @@ impl Mode { let other_options_scan = || { if let Some(width) = TerminalWidth::deduce()?.width() { - if matches.opt_present("oneline") { - if matches.opt_present("across") { - Err(Useless("across", true, "oneline")) + if matches.has(&flags::ONE_LINE) { + if matches.has(&flags::ACROSS) { + Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE)) } else { Ok(Mode::Lines) } } - else if matches.opt_present("tree") { + else if matches.has(&flags::TREE) { let details = details::Options { table: None, header: false, @@ -88,7 +90,7 @@ impl Mode { } else { let grid = grid::Options { - across: matches.opt_present("across"), + across: matches.has(&flags::ACROSS), console_width: width, }; @@ -100,7 +102,7 @@ impl Mode { // as the program’s stdout being connected to a file, then // fallback to the lines view. - if matches.opt_present("tree") { + if matches.has(&flags::TREE) { let details = details::Options { table: None, header: false, @@ -115,9 +117,9 @@ impl Mode { } }; - if matches.opt_present("long") { + if matches.has(&flags::LONG) { let details = long()?; - if matches.opt_present("grid") { + if matches.has(&flags::GRID) { match other_options_scan()? { Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)), others => return Ok(others), @@ -180,17 +182,17 @@ impl TerminalWidth { impl TableOptions { - fn deduce(matches: &getopts::Matches) -> Result { + fn deduce(matches: &Matches) -> Result { 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 { - let binary = matches.opt_present("binary"); - let bytes = matches.opt_present("bytes"); + fn deduce(matches: &Matches) -> Result { + 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 { + fn deduce(matches: &Matches) -> Result { pub use output::time::{DefaultFormat, ISOFormat}; const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"]; - if let Some(word) = matches.opt_str("time-style") { - match &*word { - "default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())), - "iso" => Ok(TimeFormat::ISOFormat(ISOFormat::new())), - "long-iso" => Ok(TimeFormat::LongISO), - "full-iso" => Ok(TimeFormat::FullISO), - otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)), - } + let word = match matches.get(&flags::TIME_STYLE) { + Some(w) => w, + None => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())), + }; + + if word == "default" { + Ok(TimeFormat::DefaultFormat(DefaultFormat::new())) + } + else if word == "iso" { + Ok(TimeFormat::ISOFormat(ISOFormat::new())) + } + else if word == "long-iso" { + Ok(TimeFormat::LongISO) + } + else if word == "full-iso" { + Ok(TimeFormat::FullISO) } else { - Ok(TimeFormat::DefaultFormat(DefaultFormat::new())) + Err(Misfire::bad_argument(&flags::TIME_STYLE, word, STYLES)) } } } @@ -255,29 +265,35 @@ impl TimeTypes { /// It’s valid to show more than one column by passing in more than one /// option, but passing *no* options means that the user just wants to /// see the default set. - fn deduce(matches: &getopts::Matches) -> Result { - 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 { + 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 { + fn deduce(matches: &Matches) -> Result { 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 { + fn deduce(matches: &Matches) -> Result { 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 } } } diff --git a/xtests/run.sh b/xtests/run.sh index 36046848..1b9d0b31 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -78,7 +78,7 @@ COLUMNS=80 $exa $testcases/file-names -R 2>&1 | diff -q - $results/file_names_R $exa $testcases/file-names -T 2>&1 | diff -q - $results/file_names_T || exit 1 # At least make sure it handles invalid UTF-8 arguments without crashing -$exa $testcases/file-names/* 2>/dev/null +$exa $testcases/file-names/* >/dev/null || exit 1 # Sorting and extension file types