diff --git a/src/main.rs b/src/main.rs index 52b7795..1574356 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,8 +13,7 @@ use log::*; use crate::fs::{Dir, File}; use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; -use crate::options::{Options, Vars}; -pub use crate::options::{Misfire, vars}; +use crate::options::{Options, Vars, vars, OptionsResult}; use crate::output::{escape, lines, grid, grid_details, details, View, Mode}; mod fs; @@ -31,43 +30,53 @@ fn main() { logger::configure(env::var_os(vars::EXA_DEBUG)); let args: Vec<_> = env::args_os().skip(1).collect(); - match Exa::from_args(args.iter(), stdout()) { - Ok(mut exa) => { + match Options::parse(&args, &LiveVars) { + OptionsResult::Ok(options, mut input_paths) => { + + // List the current directory by default. + // (This has to be done here, otherwise git_options won’t see it.) + if input_paths.is_empty() { + input_paths = vec![ OsStr::new(".") ]; + } + + let git = git_options(&options, &input_paths); + let writer = stdout(); + let exa = Exa { options, writer, input_paths, git }; + match exa.run() { Ok(exit_status) => { - exit(exit_status) + exit(exit_status); + } + + Err(e) if e.kind() == ErrorKind::BrokenPipe => { + warn!("Broken pipe error: {}", e); + exit(exits::SUCCESS); } Err(e) => { - match e.kind() { - ErrorKind::BrokenPipe => { - exit(exits::SUCCESS); - } - - _ => { - eprintln!("{}", e); - exit(exits::RUNTIME_ERROR); - } - }; + eprintln!("{}", e); + exit(exits::RUNTIME_ERROR); } - }; + } } - Err(ref e) if e.is_error() => { - let mut stderr = stderr(); - writeln!(stderr, "{}", e).unwrap(); + OptionsResult::Help(help_text) => { + println!("{}", help_text); + } - if let Some(s) = e.suggestion() { - let _ = writeln!(stderr, "{}", s); + OptionsResult::Version(version_str) => { + println!("{}", version_str); + } + + OptionsResult::InvalidOptions(error) => { + eprintln!("{}", error); + + if let Some(s) = error.suggestion() { + eprintln!("{}", s); } exit(exits::OPTIONS_ERROR); } - - Err(ref e) => { - println!("{}", e); - exit(exits::SUCCESS); - } } } @@ -83,7 +92,7 @@ pub struct Exa<'args> { /// List of the free command-line arguments that should correspond to file /// names (anything that isn’t an option). - pub args: Vec<&'args OsStr>, + pub input_paths: Vec<&'args OsStr>, /// A global Git cache, if the option was passed in. /// This has to last the lifetime of the program, because the user might @@ -113,30 +122,14 @@ fn git_options(options: &Options, args: &[&OsStr]) -> Option { } impl<'args> Exa<'args> { - pub fn from_args(args: I, writer: Stdout) -> Result, Misfire> - where I: Iterator - { - let (options, mut args) = Options::parse(args, &LiveVars)?; - debug!("Dir action from arguments: {:#?}", options.dir_action); - debug!("Filter from arguments: {:#?}", options.filter); - debug!("View from arguments: {:#?}", options.view.mode); + pub fn run(mut self) -> IOResult { + debug!("Running with options: {:#?}", self.options); - // List the current directory by default, like ls. - // This has to be done here, otherwise git_options won’t see it. - if args.is_empty() { - args = vec![ OsStr::new(".") ]; - } - - let git = git_options(&options, &args); - Ok(Exa { options, writer, args, git }) - } - - pub fn run(&mut self) -> IOResult { let mut files = Vec::new(); let mut dirs = Vec::new(); let mut exit_status = 0; - for file_path in &self.args { + for file_path in &self.input_paths { match File::from_args(PathBuf::from(file_path), None, None) { Err(e) => { exit_status = 2; diff --git a/src/options/dir_action.rs b/src/options/dir_action.rs index c16cff2..dc92a87 100644 --- a/src/options/dir_action.rs +++ b/src/options/dir_action.rs @@ -1,7 +1,7 @@ //! Parsing the options for `DirAction`. use crate::options::parser::MatchedFlags; -use crate::options::{flags, Misfire}; +use crate::options::{flags, OptionsError}; use crate::fs::dir_action::{DirAction, RecurseOptions}; @@ -12,7 +12,7 @@ impl DirAction { /// There are three possible actions, and they overlap somewhat: the /// `--tree` flag is another form of recursion, so those two are allowed /// to both be present, but the `--list-dirs` flag is used separately. - pub fn deduce(matches: &MatchedFlags) -> Result { + pub fn deduce(matches: &MatchedFlags) -> Result { let recurse = matches.has(&flags::RECURSE)?; let as_file = matches.has(&flags::LIST_DIRS)?; let tree = matches.has(&flags::TREE)?; @@ -20,13 +20,13 @@ impl DirAction { if matches.is_strict() { // Early check for --level when it wouldn’t do anything if ! recurse && ! tree && matches.count(&flags::LEVEL) > 0 { - return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)); + return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)); } else if recurse && as_file { - return Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)); + return Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS)); } else if tree && as_file { - return Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS)); + return Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS)); } } @@ -52,11 +52,11 @@ impl RecurseOptions { /// flag’s value, and whether the `--tree` flag was passed, which was /// determined earlier. The maximum level should be a number, and this /// will fail with an `Err` if it isn’t. - pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result { + pub fn deduce(matches: &MatchedFlags, 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)), + Err(e) => return Err(OptionsError::FailedParse(e)), } } else { @@ -115,12 +115,12 @@ mod test { test!(dirs_tree: DirAction <- ["--list-dirs", "--tree"]; Last => Ok(Recurse(RecurseOptions { tree: true, max_depth: None }))); test!(just_level: DirAction <- ["--level=4"]; Last => Ok(DirAction::List)); - test!(dirs_recurse_2: DirAction <- ["--list-dirs", "--recurse"]; Complain => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS))); - test!(dirs_tree_2: DirAction <- ["--list-dirs", "--tree"]; Complain => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS))); - test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))); + test!(dirs_recurse_2: DirAction <- ["--list-dirs", "--recurse"]; Complain => Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS))); + test!(dirs_tree_2: DirAction <- ["--list-dirs", "--tree"]; Complain => Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS))); + test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))); // Overriding levels test!(overriding_1: DirAction <- ["-RL=6", "-L=7"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(7) }))); - test!(overriding_2: DirAction <- ["-RL=6", "-L=7"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'L'), Flag::Short(b'L')))); + test!(overriding_2: DirAction <- ["-RL=6", "-L=7"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'L'), Flag::Short(b'L')))); } diff --git a/src/options/misfire.rs b/src/options/error.rs similarity index 65% rename from src/options/misfire.rs rename to src/options/error.rs index 2bfae85..201a20a 100644 --- a/src/options/misfire.rs +++ b/src/options/error.rs @@ -2,17 +2,16 @@ use std::ffi::OsString; use std::fmt; use std::num::ParseIntError; -use crate::options::{flags, HelpString, VersionString}; +use crate::options::flags; use crate::options::parser::{Arg, Flag, ParseError}; -/// A **misfire** is a thing that can happen instead of listing files — a -/// catch-all for anything outside the program’s normal execution. +/// Something wrong with the combination of options the user has picked. #[derive(PartialEq, Debug)] -pub enum Misfire { +pub enum OptionsError { - /// The getopts crate didn’t like these Arguments. - InvalidOptions(ParseError), + /// There was an error (from `getopts`) parsing the arguments. + Parse(ParseError), /// The user supplied an illegal choice to an Argument. BadArgument(&'static Arg, OsString), @@ -20,13 +19,6 @@ pub enum Misfire { /// The user supplied a set of options Unsupported(String), - /// The user asked for help. This isn’t strictly an error, which is why - /// this enum isn’t named Error! - Help(HelpString), - - /// The user wanted the version number. - Version(VersionString), - /// An option was given twice or more in strict mode. Duplicate(Flag, Flag), @@ -51,21 +43,13 @@ pub enum Misfire { FailedGlobPattern(String), } -impl Misfire { - - /// The OS return code this misfire should signify. - pub fn is_error(&self) -> bool { - ! matches!(self, Self::Help(_) | Self::Version(_)) - } -} - -impl From for Misfire { +impl From for OptionsError { fn from(error: glob::PatternError) -> Self { Self::FailedGlobPattern(error.to_string()) } } -impl fmt::Display for Misfire { +impl fmt::Display for OptionsError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use crate::options::parser::TakesValue; @@ -77,11 +61,9 @@ impl fmt::Display for Misfire { else { write!(f, "Option {} has no {:?} setting", arg, attempt) } - }, - Self::InvalidOptions(e) => write!(f, "{}", e), + } + Self::Parse(e) => write!(f, "{}", e), Self::Unsupported(e) => write!(f, "{}", e), - Self::Help(text) => write!(f, "{}", text), - Self::Version(version) => write!(f, "{}", version), Self::Conflict(a, b) => write!(f, "Option {} conflicts with option {}", a, b), Self::Duplicate(a, b) if a == b => write!(f, "Flag {} was given twice", a), Self::Duplicate(a, b) => write!(f, "Flag {} conflicts with flag {}", a, b), @@ -95,19 +77,8 @@ impl fmt::Display for Misfire { } } -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::NeedsValue { flag, values: None } => write!(f, "Flag {} needs a value", flag), - Self::NeedsValue { flag, values: Some(cs) } => write!(f, "Flag {} needs a value ({})", flag, Choices(cs)), - Self::ForbiddenValue { flag } => write!(f, "Flag {} cannot take a value", flag), - Self::UnknownShortArgument { attempt } => write!(f, "Unknown argument -{}", *attempt as char), - Self::UnknownArgument { attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()), - } - } -} +impl OptionsError { -impl Misfire { /// Try to second-guess what the user was trying to do, depending on what /// went wrong. pub fn suggestion(&self) -> Option<&'static str> { @@ -116,7 +87,7 @@ impl Misfire { Self::BadArgument(time, r) if *time == &flags::TIME && r == "r" => { Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\"") } - Self::InvalidOptions(ParseError::NeedsValue { ref flag, .. }) if *flag == Flag::Short(b't') => { + Self::Parse(ParseError::NeedsValue { ref flag, .. }) if *flag == Flag::Short(b't') => { Some("To sort newest files last, try \"--sort newest\", or just \"-snew\"") } _ => { @@ -129,7 +100,7 @@ impl Misfire { /// A list of legal choices for an argument-taking option. #[derive(PartialEq, Debug)] -pub struct Choices(&'static [&'static str]); +pub struct Choices(pub &'static [&'static str]); impl fmt::Display for Choices { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/src/options/filter.rs b/src/options/filter.rs index fd21b6f..44f91f6 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -3,14 +3,14 @@ use crate::fs::DotFilter; use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore}; -use crate::options::{flags, Misfire}; +use crate::options::{flags, OptionsError}; use crate::options::parser::MatchedFlags; impl FileFilter { /// Determines which of all the file filter options to use. - pub fn deduce(matches: &MatchedFlags) -> Result { + pub fn deduce(matches: &MatchedFlags) -> Result { Ok(Self { list_dirs_first: matches.has(&flags::DIRS_FIRST)?, reverse: matches.has(&flags::REVERSE)?, @@ -29,7 +29,7 @@ impl SortField { /// This argument’s value can be one of several flags, listed above. /// Returns the default sort field if none is given, or `Err` if the /// value doesn’t correspond to a sort field we know about. - fn deduce(matches: &MatchedFlags) -> Result { + fn deduce(matches: &MatchedFlags) -> Result { let word = match matches.get(&flags::SORT)? { Some(w) => w, None => return Ok(Self::default()), @@ -38,7 +38,7 @@ impl SortField { // Get String because we can’t match an OsStr let word = match word.to_str() { Some(w) => w, - None => return Err(Misfire::BadArgument(&flags::SORT, word.into())) + None => return Err(OptionsError::BadArgument(&flags::SORT, word.into())) }; let field = match word { @@ -98,7 +98,7 @@ impl SortField { Self::Unsorted } _ => { - return Err(Misfire::BadArgument(&flags::SORT, word.into())); + return Err(OptionsError::BadArgument(&flags::SORT, word.into())); } }; @@ -153,7 +153,7 @@ impl DotFilter { /// It also checks for the `--tree` option in strict mode, because of a /// special case where `--tree --all --all` won’t work: listing the /// parent directory in tree mode would loop onto itself! - pub fn deduce(matches: &MatchedFlags) -> Result { + pub fn deduce(matches: &MatchedFlags) -> Result { let count = matches.count(&flags::ALL); if count == 0 { @@ -163,10 +163,10 @@ impl DotFilter { Ok(Self::Dotfiles) } else if matches.count(&flags::TREE) > 0 { - Err(Misfire::TreeAllAll) + Err(OptionsError::TreeAllAll) } else if count >= 3 && matches.is_strict() { - Err(Misfire::Conflict(&flags::ALL, &flags::ALL)) + Err(OptionsError::Conflict(&flags::ALL, &flags::ALL)) } else { Ok(Self::DotfilesAndDots) @@ -180,7 +180,7 @@ impl IgnorePatterns { /// Determines the set of glob patterns to use based on the /// `--ignore-glob` argument’s value. This is a list of strings /// separated by pipe (`|`) characters, given in any order. - pub fn deduce(matches: &MatchedFlags) -> Result { + pub fn deduce(matches: &MatchedFlags) -> Result { // If there are no inputs, we return a set of patterns that doesn’t // match anything, rather than, say, `None`. @@ -204,7 +204,7 @@ impl IgnorePatterns { impl GitIgnore { - pub fn deduce(matches: &MatchedFlags) -> Result { + pub fn deduce(matches: &MatchedFlags) -> Result { if matches.has(&flags::GIT_IGNORE)? { Ok(Self::CheckAndIgnore) } @@ -260,13 +260,13 @@ mod test { test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc))); // Errors - test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::BadArgument(&flags::SORT, OsString::from("colour")))); + test!(error: SortField <- ["--sort=colour"]; Both => Err(OptionsError::BadArgument(&flags::SORT, OsString::from("colour")))); // Overriding test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate)); test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc))); - test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); - test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); + test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); + test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); } @@ -282,12 +282,12 @@ mod test { test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots)); test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots)); - test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(Misfire::Conflict(&flags::ALL, &flags::ALL))); + test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))); // --all and --tree test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles)); - test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(Misfire::TreeAllAll)); - test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(Misfire::TreeAllAll)); + test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll)); + test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll)); } @@ -309,8 +309,8 @@ mod test { // Overriding test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ]))); test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ]))); - test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); - test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); + test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); + test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); } diff --git a/src/options/help.rs b/src/options/help.rs index 4bb551c..5cc2f63 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -86,15 +86,15 @@ impl HelpString { /// We don’t do any strict-mode error checking here: it’s OK to give /// the --help or --long flags more than once. Actually checking for /// errors when the user wants help is kind of petty! - pub fn deduce(matches: &MatchedFlags) -> Result<(), Self> { + pub fn deduce(matches: &MatchedFlags) -> Option { if matches.count(&flags::HELP) > 0 { let only_long = matches.count(&flags::LONG) > 0; let git = cfg!(feature="git"); let xattrs = xattr::ENABLED; - Err(Self { only_long, git, xattrs }) + Some(Self { only_long, git, xattrs }) } else { - Ok(()) // no help needs to be shown + None } } } @@ -129,7 +129,7 @@ impl fmt::Display for HelpString { #[cfg(test)] mod test { - use crate::options::Options; + use crate::options::{Options, OptionsResult}; use std::ffi::OsString; fn os(input: &'static str) -> OsString { @@ -142,20 +142,20 @@ mod test { fn help() { let args = [ os("--help") ]; let opts = Options::parse(&args, &None); - assert!(opts.is_err()) + assert!(matches!(opts, OptionsResult::Help(_))); } #[test] fn help_with_file() { let args = [ os("--help"), os("me") ]; let opts = Options::parse(&args, &None); - assert!(opts.is_err()) + assert!(matches!(opts, OptionsResult::Help(_))); } #[test] fn unhelpful() { let args = []; let opts = Options::parse(&args, &None); - assert!(opts.is_ok()) // no help when --help isn’t passed + assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isn’t passed } } diff --git a/src/options/mod.rs b/src/options/mod.rs index ba25a0e..29a3a8f 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -81,20 +81,20 @@ mod flags; mod style; mod view; +mod error; +pub use self::error::OptionsError; + mod help; use self::help::HelpString; -mod version; -use self::version::VersionString; - -mod misfire; -pub use self::misfire::Misfire; +mod parser; +use self::parser::MatchedFlags; pub mod vars; pub use self::vars::Vars; -mod parser; -use self::parser::MatchedFlags; +mod version; +use self::version::VersionString; /// These **options** represent a parsed, error-checked versions of the @@ -119,9 +119,9 @@ impl Options { /// struct and a list of free filenames, using the environment variables /// for extra options. #[allow(unused_results)] - pub fn parse<'args, I, V>(args: I, vars: &V) -> Result<(Self, Vec<&'args OsStr>), Misfire> + pub fn parse<'args, I, V>(args: I, vars: &V) -> OptionsResult<'args> where I: IntoIterator, - V: Vars + V: Vars, { use crate::options::parser::{Matches, Strictness}; @@ -132,15 +132,22 @@ impl Options { }; let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) { - Ok(m) => m, - Err(e) => return Err(Misfire::InvalidOptions(e)), + Ok(m) => m, + Err(pe) => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)), }; - HelpString::deduce(&flags).map_err(Misfire::Help)?; - VersionString::deduce(&flags).map_err(Misfire::Version)?; + if let Some(help) = HelpString::deduce(&flags) { + return OptionsResult::Help(help); + } - let options = Self::deduce(&flags, vars)?; - Ok((options, frees)) + if let Some(version) = VersionString::deduce(&flags) { + return OptionsResult::Version(version); + } + + match Self::deduce(&flags, vars) { + Ok(options) => OptionsResult::Ok(options, frees), + Err(oe) => OptionsResult::InvalidOptions(oe), + } } /// Whether the View specified in this set of options includes a Git @@ -160,7 +167,7 @@ impl Options { /// Determines the complete set of options based on the given command-line /// arguments, after they’ve been parsed. - fn deduce(matches: &MatchedFlags, vars: &V) -> Result { + fn deduce(matches: &MatchedFlags, vars: &V) -> Result { let dir_action = DirAction::deduce(matches)?; let filter = FileFilter::deduce(matches)?; let view = View::deduce(matches, vars)?; @@ -170,9 +177,26 @@ impl Options { } +/// The result of the `Options::getopts` function. +#[derive(Debug)] +pub enum OptionsResult<'args> { + + /// The options were parsed successfully. + Ok(Options, Vec<&'args OsStr>), + + /// There was an error parsing the arguments. + InvalidOptions(OptionsError), + + /// One of the arguments was `--help`, so display help. + Help(HelpString), + + /// One of the arguments was `--version`, so display the version number. + Version(VersionString), +} + + #[cfg(test)] pub mod test { - use super::{Options, Misfire, flags}; use crate::options::parser::{Arg, MatchedFlags}; use std::ffi::OsString; @@ -218,32 +242,4 @@ pub mod test { os.push(input); os } - - #[test] - fn files() { - let args = [ os("this file"), os("that file") ]; - let outs = Options::parse(&args, &None).unwrap().1; - assert_eq!(outs, vec![ &os("this file"), &os("that file") ]) - } - - #[test] - fn no_args() { - let nothing: Vec = Vec::new(); - let outs = Options::parse(¬hing, &None).unwrap().1; - assert!(outs.is_empty()); // Listing the `.` directory is done in main.rs - } - - #[test] - fn long_across() { - let args = [ os("--long"), os("--across") ]; - let opts = Options::parse(&args, &None); - assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::LONG)) - } - - #[test] - fn oneline_across() { - let args = [ os("--oneline"), os("--across") ]; - let opts = Options::parse(&args, &None); - assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE)) - } } diff --git a/src/options/parser.rs b/src/options/parser.rs index 7d2cd6c..26bf2f8 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -31,7 +31,7 @@ use std::ffi::{OsStr, OsString}; use std::fmt; -use crate::options::Misfire; +use crate::options::error::{OptionsError, Choices}; /// A **short argument** is a single ASCII character. @@ -373,7 +373,7 @@ impl<'a> MatchedFlags<'a> { /// Whether the given argument was specified. /// Returns `true` if it was, `false` if it wasn’t, and an error in /// strict mode if it was specified more than once. - pub fn has(&self, arg: &'static Arg) -> Result { + pub fn has(&self, arg: &'static Arg) -> Result { self.has_where(|flag| flag.matches(arg)) .map(|flag| flag.is_some()) } @@ -383,7 +383,7 @@ impl<'a> MatchedFlags<'a> { /// argument satisfy the predicate. /// /// You’ll have to test the resulting flag to see which argument it was. - pub fn has_where

(&self, predicate: P) -> Result, Misfire> + pub fn has_where

(&self, predicate: P) -> Result, OptionsError> where P: Fn(&Flag) -> bool { if self.is_strict() { let all = self.flags.iter() @@ -391,7 +391,7 @@ impl<'a> MatchedFlags<'a> { .collect::>(); if all.len() < 2 { Ok(all.first().map(|t| &t.0)) } - else { Err(Misfire::Duplicate(all[0].0, all[1].0)) } + else { Err(OptionsError::Duplicate(all[0].0, all[1].0)) } } else { let any = self.flags.iter().rev() @@ -408,7 +408,7 @@ impl<'a> MatchedFlags<'a> { /// Returns the value of the given argument if it was specified, nothing /// if it wasn’t, and an error in strict mode if it was specified more /// than once. - pub fn get(&self, arg: &'static Arg) -> Result, Misfire> { + pub fn get(&self, arg: &'static Arg) -> Result, OptionsError> { self.get_where(|flag| flag.matches(arg)) } @@ -417,7 +417,7 @@ impl<'a> MatchedFlags<'a> { /// multiple arguments matched the predicate. /// /// It’s not possible to tell which flag the value belonged to from this. - pub fn get_where

(&self, predicate: P) -> Result, Misfire> + pub fn get_where

(&self, predicate: P) -> Result, OptionsError> where P: Fn(&Flag) -> bool { if self.is_strict() { let those = self.flags.iter() @@ -425,7 +425,7 @@ impl<'a> MatchedFlags<'a> { .collect::>(); if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) } - else { Err(Misfire::Duplicate(those[0].0, those[1].0)) } + else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) } } else { let found = self.flags.iter().rev() @@ -475,10 +475,17 @@ pub enum ParseError { 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. +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::NeedsValue { flag, values: None } => write!(f, "Flag {} needs a value", flag), + Self::NeedsValue { flag, values: Some(cs) } => write!(f, "Flag {} needs a value ({})", flag, Choices(cs)), + Self::ForbiddenValue { flag } => write!(f, "Flag {} cannot take a value", flag), + Self::UnknownShortArgument { attempt } => write!(f, "Unknown argument -{}", *attempt as char), + Self::UnknownArgument { attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()), + } + } +} /// Splits a string on its `=` character, returning the two substrings on diff --git a/src/options/style.rs b/src/options/style.rs index c877276..b931b24 100644 --- a/src/options/style.rs +++ b/src/options/style.rs @@ -1,7 +1,7 @@ use ansi_term::Style; use crate::fs::File; -use crate::options::{flags, Vars, Misfire}; +use crate::options::{flags, Vars, OptionsError}; use crate::options::parser::MatchedFlags; use crate::output::file_name::{Classify, FileStyle}; use crate::style::Colours; @@ -37,7 +37,7 @@ impl Default for TerminalColours { impl TerminalColours { /// Determine which terminal colour conditions to use. - fn deduce(matches: &MatchedFlags) -> Result { + fn deduce(matches: &MatchedFlags) -> Result { let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? { Some(w) => w, None => return Ok(Self::default()), @@ -53,7 +53,7 @@ impl TerminalColours { Ok(Self::Never) } else { - Err(Misfire::BadArgument(&flags::COLOR, word.into())) + Err(OptionsError::BadArgument(&flags::COLOR, word.into())) } } } @@ -77,7 +77,7 @@ pub struct Styles { impl Styles { #[allow(trivial_casts)] // the `as Box<_>` stuff below warns about this for some reason - pub fn deduce(matches: &MatchedFlags, vars: &V, widther: TW) -> Result + pub fn deduce(matches: &MatchedFlags, vars: &V, widther: TW) -> Result where TW: Fn() -> Option, V: Vars { use crate::info::filetype::FileExtensions; use crate::output::file_name::NoFileColours; @@ -204,7 +204,7 @@ impl ExtensionMappings { impl Classify { - fn deduce(matches: &MatchedFlags) -> Result { + fn deduce(matches: &MatchedFlags) -> Result { let flagged = matches.has(&flags::CLASSIFY)?; if flagged { Ok(Self::AddFileIndicators) } @@ -260,8 +260,8 @@ mod terminal_test { test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never)); // Errors - test!(no_u_error: ["--color=upstream"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color - test!(u_error: ["--colour=lovers"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one! + test!(no_u_error: ["--color=upstream"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color + test!(u_error: ["--colour=lovers"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one! // Overriding test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never)); @@ -269,10 +269,10 @@ mod terminal_test { test!(overridden_3: ["--colour=auto", "--color=never"]; Last => Ok(TerminalColours::Never)); test!(overridden_4: ["--color=auto", "--color=never"]; Last => Ok(TerminalColours::Never)); - test!(overridden_5: ["--colour=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("colour"))); - test!(overridden_6: ["--color=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("colour"))); - test!(overridden_7: ["--colour=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("color"))); - test!(overridden_8: ["--color=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("color"))); + test!(overridden_5: ["--colour=auto", "--colour=never"]; Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour"))); + test!(overridden_6: ["--color=auto", "--colour=never"]; Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("colour"))); + test!(overridden_7: ["--colour=auto", "--color=never"]; Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color"))); + test!(overridden_8: ["--color=auto", "--color=never"]; Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("color"))); } @@ -335,7 +335,7 @@ mod colour_test { test!(scale_3: ["--color=always", "--colour-scale"], || None; Last => Ok(Colours::colourful(true))); test!(scale_4: ["--color=always", ], || None; Last => Ok(Colours::colourful(false))); - test!(scale_5: ["--color=always", "--color-scale", "--colour-scale"], || None; Complain => err Misfire::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale"))); + test!(scale_5: ["--color=always", "--color-scale", "--colour-scale"], || None; Complain => err OptionsError::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale"))); test!(scale_6: ["--color=always", "--color-scale", ], || None; Complain => Ok(Colours::colourful(true))); test!(scale_7: ["--color=always", "--colour-scale"], || None; Complain => Ok(Colours::colourful(true))); test!(scale_8: ["--color=always", ], || None; Complain => Ok(Colours::colourful(false))); diff --git a/src/options/version.rs b/src/options/version.rs index 0ec1836..e23eadc 100644 --- a/src/options/version.rs +++ b/src/options/version.rs @@ -18,13 +18,13 @@ impl VersionString { /// command-line arguments. This one works backwards from the other /// ‘deduce’ functions, returning Err if help needs to be shown. /// - /// Like --help, this doesn’t bother checking for errors. - pub fn deduce(matches: &MatchedFlags) -> Result<(), Self> { + /// Like --help, this doesn’t check for errors. + pub fn deduce(matches: &MatchedFlags) -> Option { if matches.count(&flags::VERSION) > 0 { - Err(Self) + Some(Self) } else { - Ok(()) // no version needs to be shown + None } } } @@ -38,7 +38,7 @@ impl fmt::Display for VersionString { #[cfg(test)] mod test { - use crate::options::Options; + use crate::options::{Options, OptionsResult}; use std::ffi::OsString; fn os(input: &'static str) -> OsString { @@ -48,9 +48,16 @@ mod test { } #[test] - fn help() { + fn version() { let args = [ os("--version") ]; let opts = Options::parse(&args, &None); - assert!(opts.is_err()) + assert!(matches!(opts, OptionsResult::Version(_))); + } + + #[test] + fn version_with_file() { + let args = [ os("--version"), os("me") ]; + let opts = Options::parse(&args, &None); + assert!(matches!(opts, OptionsResult::Version(_))); } } diff --git a/src/options/view.rs b/src/options/view.rs index 3712ee0..15eb87f 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -1,18 +1,18 @@ use lazy_static::lazy_static; use crate::fs::feature::xattr; -use crate::options::{flags, Misfire, Vars}; +use crate::options::{flags, OptionsError, Vars}; use crate::options::parser::MatchedFlags; use crate::output::{View, Mode, grid, details, lines}; use crate::output::grid_details::{self, RowThreshold}; -use crate::output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions}; +use crate::output::table::{TimeTypes, SizeFormat, Columns, Options as TableOptions}; use crate::output::time::TimeFormat; impl View { /// Determine which view to use and all of that view’s arguments. - pub fn deduce(matches: &MatchedFlags, vars: &V) -> Result { + pub fn deduce(matches: &MatchedFlags, vars: &V) -> Result { use crate::options::style::Styles; let mode = Mode::deduce(matches, vars)?; @@ -25,13 +25,13 @@ impl View { impl Mode { /// Determine the mode from the command-line arguments. - pub fn deduce(matches: &MatchedFlags, vars: &V) -> Result { + pub fn deduce(matches: &MatchedFlags, vars: &V) -> Result { let long = || { if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? { - Err(Misfire::Useless(&flags::ACROSS, true, &flags::LONG)) + Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG)) } else if matches.has(&flags::ONE_LINE)? { - Err(Misfire::Useless(&flags::ONE_LINE, true, &flags::LONG)) + Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG)) } else { Ok(details::Options { @@ -47,7 +47,7 @@ impl Mode { if let Some(width) = TerminalWidth::deduce(vars)?.width() { if matches.has(&flags::ONE_LINE)? { if matches.has(&flags::ACROSS)? { - Err(Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE)) + Err(OptionsError::Useless(&flags::ACROSS, true, &flags::ONE_LINE)) } else { let lines = lines::Options { icons: matches.has(&flags::ICONS)? }; @@ -121,17 +121,17 @@ impl Mode { 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(Misfire::Useless(*option, false, &flags::LONG)); + return Err(OptionsError::Useless(*option, false, &flags::LONG)); } } if cfg!(feature = "git") && matches.has(&flags::GIT)? { - return Err(Misfire::Useless(&flags::GIT, false, &flags::LONG)); + return Err(OptionsError::Useless(&flags::GIT, false, &flags::LONG)); } else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? { // TODO: I’m not sure if the code even gets this far. // There is an identical check in dir_action - return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)); + return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)); } } @@ -159,13 +159,13 @@ impl TerminalWidth { /// Determine a requested terminal width from the command-line arguments. /// /// Returns an error if a requested width doesn’t parse to an integer. - fn deduce(vars: &V) -> Result { + fn deduce(vars: &V) -> Result { use crate::options::vars; if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) { match columns.parse() { Ok(width) => Ok(Self::Set(width)), - Err(e) => Err(Misfire::FailedParse(e)), + Err(e) => Err(OptionsError::FailedParse(e)), } } else if let Some(width) = *TERM_WIDTH { @@ -190,13 +190,13 @@ impl RowThreshold { /// Determine whether to use a row threshold based on the given /// environment variables. - fn deduce(vars: &V) -> Result { + fn deduce(vars: &V) -> Result { use crate::options::vars; if let Some(columns) = vars.get(vars::EXA_GRID_ROWS).and_then(|s| s.into_string().ok()) { match columns.parse() { Ok(rows) => Ok(Self::MinimumRows(rows)), - Err(e) => Err(Misfire::FailedParse(e)), + Err(e) => Err(OptionsError::FailedParse(e)), } } else { @@ -207,18 +207,17 @@ impl RowThreshold { impl TableOptions { - fn deduce(matches: &MatchedFlags, vars: &V) -> Result { - let env = Environment::load_all(); + fn deduce(matches: &MatchedFlags, vars: &V) -> Result { let time_format = TimeFormat::deduce(matches, vars)?; let size_format = SizeFormat::deduce(matches)?; let columns = Columns::deduce(matches)?; - Ok(Self { env, time_format, size_format, columns }) + Ok(Self { time_format, size_format, columns }) } } impl Columns { - fn deduce(matches: &MatchedFlags) -> Result { + fn deduce(matches: &MatchedFlags) -> Result { let time_types = TimeTypes::deduce(matches)?; let git = cfg!(feature = "git") && matches.has(&flags::GIT)?; @@ -247,7 +246,7 @@ 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: &MatchedFlags) -> Result { + fn deduce(matches: &MatchedFlags) -> Result { let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?; Ok(match flag { @@ -262,9 +261,7 @@ impl SizeFormat { impl TimeFormat { /// Determine how time should be formatted in timestamp columns. - fn deduce(matches: &MatchedFlags, vars: &V) -> Result { - pub use crate::output::time::{DefaultFormat, ISOFormat}; - + fn deduce(matches: &MatchedFlags, vars: &V) -> Result { let word = match matches.get(&flags::TIME_STYLE)? { Some(w) => { w.to_os_string() @@ -273,16 +270,16 @@ impl TimeFormat { use crate::options::vars; match vars.get(vars::TIME_STYLE) { Some(ref t) if ! t.is_empty() => t.clone(), - _ => return Ok(Self::DefaultFormat(DefaultFormat::load())) + _ => return Ok(Self::DefaultFormat) } }, }; if &word == "default" { - Ok(Self::DefaultFormat(DefaultFormat::load())) + Ok(Self::DefaultFormat) } else if &word == "iso" { - Ok(Self::ISOFormat(ISOFormat::load())) + Ok(Self::ISOFormat) } else if &word == "long-iso" { Ok(Self::LongISO) @@ -291,7 +288,7 @@ impl TimeFormat { Ok(Self::FullISO) } else { - Err(Misfire::BadArgument(&flags::TIME_STYLE, word)) + Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)) } } } @@ -309,7 +306,7 @@ 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: &MatchedFlags) -> Result { + fn deduce(matches: &MatchedFlags) -> Result { let possible_word = matches.get(&flags::TIME)?; let modified = matches.has(&flags::MODIFIED)?; let changed = matches.has(&flags::CHANGED)?; @@ -322,16 +319,16 @@ impl TimeTypes { Self { modified: false, changed: false, accessed: false, created: false } } else if let Some(word) = possible_word { if modified { - return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME)); + return Err(OptionsError::Useless(&flags::MODIFIED, true, &flags::TIME)); } else if changed { - return Err(Misfire::Useless(&flags::CHANGED, true, &flags::TIME)); + return Err(OptionsError::Useless(&flags::CHANGED, true, &flags::TIME)); } else if accessed { - return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME)); + return Err(OptionsError::Useless(&flags::ACCESSED, true, &flags::TIME)); } else if created { - return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME)); + return Err(OptionsError::Useless(&flags::CREATED, true, &flags::TIME)); } else if word == "mod" || word == "modified" { Self { modified: true, changed: false, accessed: false, created: false } @@ -346,7 +343,7 @@ impl TimeTypes { Self { modified: false, changed: false, accessed: false, created: true } } else { - return Err(Misfire::BadArgument(&flags::TIME, word.into())); + return Err(OptionsError::BadArgument(&flags::TIME, word.into())); } } else if modified || changed || accessed || created { @@ -474,10 +471,10 @@ mod test { test!(both_3: SizeFormat <- ["--binary", "--bytes"]; Last => Ok(SizeFormat::JustBytes)); test!(both_4: SizeFormat <- ["--bytes", "--bytes"]; Last => Ok(SizeFormat::JustBytes)); - test!(both_5: SizeFormat <- ["--binary", "--binary"]; Complain => err Misfire::Duplicate(Flag::Long("binary"), Flag::Long("binary"))); - test!(both_6: SizeFormat <- ["--bytes", "--binary"]; Complain => err Misfire::Duplicate(Flag::Long("bytes"), Flag::Long("binary"))); - test!(both_7: SizeFormat <- ["--binary", "--bytes"]; Complain => err Misfire::Duplicate(Flag::Long("binary"), Flag::Long("bytes"))); - test!(both_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err Misfire::Duplicate(Flag::Long("bytes"), Flag::Long("bytes"))); + test!(both_5: SizeFormat <- ["--binary", "--binary"]; Complain => err OptionsError::Duplicate(Flag::Long("binary"), Flag::Long("binary"))); + test!(both_6: SizeFormat <- ["--bytes", "--binary"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("binary"))); + test!(both_7: SizeFormat <- ["--binary", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("binary"), Flag::Long("bytes"))); + test!(both_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("bytes"))); } @@ -488,23 +485,23 @@ mod test { // implement PartialEq. // Default behaviour - test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat(_))); + test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat)); // Individual settings - test!(default: TimeFormat <- ["--time-style=default"], None; Both => like Ok(TimeFormat::DefaultFormat(_))); - test!(iso: TimeFormat <- ["--time-style", "iso"], None; Both => like Ok(TimeFormat::ISOFormat(_))); + test!(default: TimeFormat <- ["--time-style=default"], None; Both => like Ok(TimeFormat::DefaultFormat)); + test!(iso: TimeFormat <- ["--time-style", "iso"], None; Both => like Ok(TimeFormat::ISOFormat)); test!(long_iso: TimeFormat <- ["--time-style=long-iso"], None; Both => like Ok(TimeFormat::LongISO)); test!(full_iso: TimeFormat <- ["--time-style", "full-iso"], None; Both => like Ok(TimeFormat::FullISO)); // Overriding - test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Last => like Ok(TimeFormat::ISOFormat(_))); - test!(actual_2: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); + test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Last => like Ok(TimeFormat::ISOFormat)); + test!(actual_2: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Complain => err OptionsError::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); test!(nevermind: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Last => like Ok(TimeFormat::FullISO)); - test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); + test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Complain => err OptionsError::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); // Errors - test!(daily: TimeFormat <- ["--time-style=24-hour"], None; Both => err Misfire::BadArgument(&flags::TIME_STYLE, OsString::from("24-hour"))); + test!(daily: TimeFormat <- ["--time-style=24-hour"], None; Both => err OptionsError::BadArgument(&flags::TIME_STYLE, OsString::from("24-hour"))); // `TIME_STYLE` environment variable is defined. // If the time-style argument is not given, `TIME_STYLE` is used. @@ -552,12 +549,12 @@ mod test { // Errors - test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("tea"))); - test!(t_ea: TimeTypes <- ["-tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("ea"))); + test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("tea"))); + test!(t_ea: TimeTypes <- ["-tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("ea"))); // Overriding test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); - test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err Misfire::Duplicate(Flag::Short(b't'), Flag::Short(b't'))); + test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err OptionsError::Duplicate(Flag::Short(b't'), Flag::Short(b't'))); } @@ -604,15 +601,15 @@ mod test { #[cfg(feature = "git")] test!(just_git: Mode <- ["--git"], None; Last => like Ok(Mode::Grid(_))); - test!(just_header_2: Mode <- ["--header"], None; Complain => err Misfire::Useless(&flags::HEADER, false, &flags::LONG)); - test!(just_group_2: Mode <- ["--group"], None; Complain => err Misfire::Useless(&flags::GROUP, false, &flags::LONG)); - test!(just_inode_2: Mode <- ["--inode"], None; Complain => err Misfire::Useless(&flags::INODE, false, &flags::LONG)); - test!(just_links_2: Mode <- ["--links"], None; Complain => err Misfire::Useless(&flags::LINKS, false, &flags::LONG)); - test!(just_blocks_2: Mode <- ["--blocks"], None; Complain => err Misfire::Useless(&flags::BLOCKS, false, &flags::LONG)); - test!(just_binary_2: Mode <- ["--binary"], None; Complain => err Misfire::Useless(&flags::BINARY, false, &flags::LONG)); - test!(just_bytes_2: Mode <- ["--bytes"], None; Complain => err Misfire::Useless(&flags::BYTES, false, &flags::LONG)); + test!(just_header_2: Mode <- ["--header"], None; Complain => err OptionsError::Useless(&flags::HEADER, false, &flags::LONG)); + test!(just_group_2: Mode <- ["--group"], None; Complain => err OptionsError::Useless(&flags::GROUP, false, &flags::LONG)); + test!(just_inode_2: Mode <- ["--inode"], None; Complain => err OptionsError::Useless(&flags::INODE, false, &flags::LONG)); + test!(just_links_2: Mode <- ["--links"], None; Complain => err OptionsError::Useless(&flags::LINKS, false, &flags::LONG)); + test!(just_blocks_2: Mode <- ["--blocks"], None; Complain => err OptionsError::Useless(&flags::BLOCKS, false, &flags::LONG)); + test!(just_binary_2: Mode <- ["--binary"], None; Complain => err OptionsError::Useless(&flags::BINARY, false, &flags::LONG)); + test!(just_bytes_2: Mode <- ["--bytes"], None; Complain => err OptionsError::Useless(&flags::BYTES, false, &flags::LONG)); #[cfg(feature = "git")] - test!(just_git_2: Mode <- ["--git"], None; Complain => err Misfire::Useless(&flags::GIT, false, &flags::LONG)); + test!(just_git_2: Mode <- ["--git"], None; Complain => err OptionsError::Useless(&flags::GIT, false, &flags::LONG)); } } diff --git a/src/output/details.rs b/src/output/details.rs index 0a6afb0..af68850 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -92,7 +92,7 @@ use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth}; /// /// Almost all the heavy lifting is done in a Table object, which handles the /// columns for each row. -#[derive(Debug)] +#[derive(PartialEq, Debug)] pub struct Options { /// Options specific to drawing a table. diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 970bff3..05cb006 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -19,7 +19,7 @@ use crate::output::tree::{TreeParams, TreeDepth}; use crate::style::Colours; -#[derive(Debug)] +#[derive(PartialEq, Debug)] pub struct Options { pub grid: GridOptions, pub details: DetailsOptions, @@ -33,7 +33,7 @@ pub struct Options { /// small directory of four files in four columns, the files just look spaced /// out and it’s harder to see what’s going on. So it can be enabled just for /// larger directory listings. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(PartialEq, Debug, Copy, Clone)] pub enum RowThreshold { /// Only use grid-details view if it would result in at least this many diff --git a/src/output/mod.rs b/src/output/mod.rs index 12f402f..277c69e 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -29,7 +29,7 @@ pub struct View { /// The **mode** is the “type” of output. -#[derive(Debug)] +#[derive(PartialEq, Debug)] #[allow(clippy::large_enum_variant)] pub enum Mode { Grid(grid::Options), diff --git a/src/output/table.rs b/src/output/table.rs index 007833d..882ba48 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -1,14 +1,13 @@ use std::cmp::max; use std::env; -use std::fmt; use std::ops::Deref; use std::sync::{Mutex, MutexGuard}; use datetime::TimeZone; use zoneinfo_compiled::{CompiledData, Result as TZResult}; +use lazy_static::lazy_static; use log::*; - use users::UsersCache; use crate::fs::{File, fields as f}; @@ -20,21 +19,13 @@ use crate::style::Colours; /// Options for displaying a table. +#[derive(PartialEq, Debug)] pub struct Options { - pub env: Environment, pub size_format: SizeFormat, pub time_format: TimeFormat, pub columns: Columns, } -// I had to make other types derive Debug, -// and Mutex is not that! -impl fmt::Debug for Options { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!(f, "Table({:#?})", self.columns) - } -} - /// Extra columns to display in the table. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Columns { @@ -278,7 +269,7 @@ impl Environment { self.users.lock().unwrap() } - pub fn load_all() -> Self { + fn load_all() -> Self { let tz = match determine_time_zone() { Ok(t) => { Some(t) @@ -307,6 +298,10 @@ fn determine_time_zone() -> TZResult { } } +lazy_static! { + static ref ENVIRONMENT: Environment = Environment::load_all(); +} + pub struct Table<'a> { columns: Vec, @@ -327,13 +322,14 @@ impl<'a, 'f> Table<'a> { pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> { let columns = options.columns.collect(git.is_some()); let widths = TableWidths::zero(columns.len()); + let env = &*ENVIRONMENT; Table { colours, widths, columns, git, - env: &options.env, + env, time_format: &options.time_format, size_format: options.size_format, } diff --git a/src/output/time.rs b/src/output/time.rs index 0a85d6b..1f40a19 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -2,9 +2,11 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece}; +use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece, Month}; use datetime::fmt::DateFormat; -use std::cmp; + +use lazy_static::lazy_static; +use unicode_width::UnicodeWidthStr; /// Every timestamp in exa needs to be rendered by a **time format**. @@ -23,18 +25,18 @@ use std::cmp; /// /// Currently exa does not support *custom* styles, where the user enters a /// format string in an environment variable or something. Just these four. -#[derive(Debug)] +#[derive(PartialEq, Debug)] pub enum TimeFormat { /// The **default format** uses the user’s locale to print month names, /// and specifies the timestamp down to the minute for recent times, and /// day for older times. - DefaultFormat(DefaultFormat), + DefaultFormat, /// Use the **ISO format**, which specifies the timestamp down to the /// minute for recent times, and day for older times. It uses a number - /// for the month so it doesn’t need a locale. - ISOFormat(ISOFormat), + /// for the month so it doesn’t use the locale. + ISOFormat, /// Use the **long ISO format**, which specifies the timestamp down to the /// minute using only numbers, without needing the locale or year. @@ -52,150 +54,62 @@ pub enum TimeFormat { impl TimeFormat { pub fn format_local(&self, time: SystemTime) -> String { match self { - Self::DefaultFormat(fmt) => fmt.format_local(time), - Self::ISOFormat(iso) => iso.format_local(time), - Self::LongISO => long_local(time), - Self::FullISO => full_local(time), + Self::DefaultFormat => default_local(time), + Self::ISOFormat => iso_local(time), + Self::LongISO => long_local(time), + Self::FullISO => full_local(time), } } pub fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String { match self { - Self::DefaultFormat(fmt) => fmt.format_zoned(time, zone), - Self::ISOFormat(iso) => iso.format_zoned(time, zone), - Self::LongISO => long_zoned(time, zone), - Self::FullISO => full_zoned(time, zone), + Self::DefaultFormat => default_zoned(time, zone), + Self::ISOFormat => iso_zoned(time, zone), + Self::LongISO => long_zoned(time, zone), + Self::FullISO => full_zoned(time, zone), } } } -#[derive(Debug, Clone)] -pub struct DefaultFormat { +#[allow(trivial_numeric_casts)] +fn default_local(time: SystemTime) -> String { + let date = LocalDateTime::at(systemtime_epoch(time)); - /// The year of the current time. This gets used to determine which date - /// format to use. - pub current_year: i64, - - /// Localisation rules for formatting timestamps. - pub locale: locale::Time, - - /// Date format for printing out timestamps that are in the current year. - pub date_and_time: DateFormat<'static>, - - /// Date format for printing out timestamps that *aren’t*. - pub date_and_year: DateFormat<'static>, -} - -impl DefaultFormat { - pub fn load() -> Self { - use unicode_width::UnicodeWidthStr; - - let locale = locale::Time::load_user_locale() - .unwrap_or_else(|_| locale::Time::english()); - - let current_year = LocalDateTime::now().year(); - - // Some locales use a three-character wide month name (Jan to Dec); - // others vary between three to four (1月 to 12月, juil.). We check each month width - // to detect the longest and set the output format accordingly. - let mut maximum_month_width = 0; - for i in 0..11 { - let current_month_width = UnicodeWidthStr::width(&*locale.short_month_name(i)); - maximum_month_width = cmp::max(maximum_month_width, current_month_width); - } - - let date_and_time = match maximum_month_width { - 4 => DateFormat::parse("{2>:D} {4<:M} {2>:h}:{02>:m}").unwrap(), - 5 => DateFormat::parse("{2>:D} {5<:M} {2>:h}:{02>:m}").unwrap(), - _ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(), + if date.year() == *CURRENT_YEAR { + format!("{:2} {} {:02}:{:02}", + date.day(), month_to_abbrev(date.month()), + date.hour(), date.minute()) + } + else { + let date_format = match *MAXIMUM_MONTH_WIDTH { + 4 => &*FOUR_WIDE_DATE_TIME, + 5 => &*FIVE_WIDE_DATE_TIME, + _ => &*OTHER_WIDE_DATE_TIME, }; - let date_and_year = match maximum_month_width { - 4 => DateFormat::parse("{2>:D} {4<:M} {5>:Y}").unwrap(), - 5 => DateFormat::parse("{2>:D} {5<:M} {5>:Y}").unwrap(), - _ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap() + date_format.format(&date, &*LOCALE) + } +} + +#[allow(trivial_numeric_casts)] +fn default_zoned(time: SystemTime, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); + + if date.year() == *CURRENT_YEAR { + format!("{:2} {} {:02}:{:02}", + date.day(), month_to_abbrev(date.month()), + date.hour(), date.minute()) + } + else { + let date_format = match *MAXIMUM_MONTH_WIDTH { + 4 => &*FOUR_WIDE_DATE_YEAR, + 5 => &*FIVE_WIDE_DATE_YEAR, + _ => &*OTHER_WIDE_DATE_YEAR, }; - Self { current_year, locale, date_and_time, date_and_year } + date_format.format(&date, &*LOCALE) } - - fn is_recent(&self, date: LocalDateTime) -> bool { - date.year() == self.current_year - } - - fn month_to_abbrev(month: datetime::Month) -> &'static str { - match month { - datetime::Month::January => "Jan", - datetime::Month::February => "Feb", - datetime::Month::March => "Mar", - datetime::Month::April => "Apr", - datetime::Month::May => "May", - datetime::Month::June => "Jun", - datetime::Month::July => "Jul", - datetime::Month::August => "Aug", - datetime::Month::September => "Sep", - datetime::Month::October => "Oct", - datetime::Month::November => "Nov", - datetime::Month::December => "Dec", - } - } - - #[allow(trivial_numeric_casts)] - fn format_local(&self, time: SystemTime) -> String { - let date = LocalDateTime::at(systemtime_epoch(time)); - - if self.is_recent(date) { - format!("{:2} {} {:02}:{:02}", - date.day(), Self::month_to_abbrev(date.month()), - date.hour(), date.minute()) - } - else { - self.date_and_year.format(&date, &self.locale) - } - } - - #[allow(trivial_numeric_casts)] - fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); - - if self.is_recent(date) { - format!("{:2} {} {:02}:{:02}", - date.day(), Self::month_to_abbrev(date.month()), - date.hour(), date.minute()) - } - else { - self.date_and_year.format(&date, &self.locale) - } - } -} - -fn systemtime_epoch(time: SystemTime) -> i64 { - time - .duration_since(UNIX_EPOCH) - .map(|t| t.as_secs() as i64) - .unwrap_or_else(|e| { - let diff = e.duration(); - let mut secs = diff.as_secs(); - if diff.subsec_nanos() > 0 { - secs += 1; - } - -(secs as i64) - }) -} - -fn systemtime_nanos(time: SystemTime) -> u32 { - time - .duration_since(UNIX_EPOCH) - .map(|t| t.subsec_nanos()) - .unwrap_or_else(|e| { - let nanos = e.duration().subsec_nanos(); - if nanos > 0 { - 1_000_000_000 - nanos - } else { - nanos - } - }) } #[allow(trivial_numeric_casts)] @@ -235,54 +149,127 @@ fn full_zoned(time: SystemTime, zone: &TimeZone) -> String { offset.hours(), offset.minutes().abs()) } +#[allow(trivial_numeric_casts)] +fn iso_local(time: SystemTime) -> String { + let date = LocalDateTime::at(systemtime_epoch(time)); -#[derive(Debug, Copy, Clone)] -pub struct ISOFormat { - - /// The year of the current time. This gets used to determine which date - /// format to use. - pub current_year: i64, -} - -impl ISOFormat { - pub fn load() -> Self { - let current_year = LocalDateTime::now().year(); - Self { current_year } + if is_recent(date) { + format!("{:02}-{:02} {:02}:{:02}", + date.month() as usize, date.day(), + date.hour(), date.minute()) + } + else { + format!("{:04}-{:02}-{:02}", + date.year(), date.month() as usize, date.day()) } } -impl ISOFormat { - fn is_recent(self, date: LocalDateTime) -> bool { - date.year() == self.current_year +#[allow(trivial_numeric_casts)] +fn iso_zoned(time: SystemTime, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); + + if is_recent(date) { + format!("{:02}-{:02} {:02}:{:02}", + date.month() as usize, date.day(), + date.hour(), date.minute()) } - - #[allow(trivial_numeric_casts)] - fn format_local(self, time: SystemTime) -> String { - let date = LocalDateTime::at(systemtime_epoch(time)); - - if self.is_recent(date) { - format!("{:02}-{:02} {:02}:{:02}", - date.month() as usize, date.day(), - date.hour(), date.minute()) - } - else { - format!("{:04}-{:02}-{:02}", - date.year(), date.month() as usize, date.day()) - } - } - - #[allow(trivial_numeric_casts)] - fn format_zoned(self, time: SystemTime, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); - - if self.is_recent(date) { - format!("{:02}-{:02} {:02}:{:02}", - date.month() as usize, date.day(), - date.hour(), date.minute()) - } - else { - format!("{:04}-{:02}-{:02}", - date.year(), date.month() as usize, date.day()) - } + else { + format!("{:04}-{:02}-{:02}", + date.year(), date.month() as usize, date.day()) } } + + +fn systemtime_epoch(time: SystemTime) -> i64 { + time.duration_since(UNIX_EPOCH) + .map(|t| t.as_secs() as i64) + .unwrap_or_else(|e| { + let diff = e.duration(); + let mut secs = diff.as_secs(); + if diff.subsec_nanos() > 0 { + secs += 1; + } + -(secs as i64) + }) +} + +fn systemtime_nanos(time: SystemTime) -> u32 { + time.duration_since(UNIX_EPOCH) + .map(|t| t.subsec_nanos()) + .unwrap_or_else(|e| { + let nanos = e.duration().subsec_nanos(); + if nanos > 0 { + 1_000_000_000 - nanos + } else { + nanos + } + }) +} + +fn is_recent(date: LocalDateTime) -> bool { + date.year() == *CURRENT_YEAR +} + +fn month_to_abbrev(month: Month) -> &'static str { + match month { + Month::January => "Jan", + Month::February => "Feb", + Month::March => "Mar", + Month::April => "Apr", + Month::May => "May", + Month::June => "Jun", + Month::July => "Jul", + Month::August => "Aug", + Month::September => "Sep", + Month::October => "Oct", + Month::November => "Nov", + Month::December => "Dec", + } +} + + +lazy_static! { + + static ref CURRENT_YEAR: i64 = LocalDateTime::now().year(); + + static ref LOCALE: locale::Time = { + locale::Time::load_user_locale() + .unwrap_or_else(|_| locale::Time::english()) + }; + + static ref MAXIMUM_MONTH_WIDTH: usize = { + // Some locales use a three-character wide month name (Jan to Dec); + // others vary between three to four (1月 to 12月, juil.). We check each month width + // to detect the longest and set the output format accordingly. + let mut maximum_month_width = 0; + for i in 0..11 { + let current_month_width = UnicodeWidthStr::width(&*LOCALE.short_month_name(i)); + maximum_month_width = std::cmp::max(maximum_month_width, current_month_width); + } + maximum_month_width + }; + + static ref FOUR_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse( + "{2>:D} {4<:M} {2>:h}:{02>:m}" + ).unwrap(); + + static ref FIVE_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse( + "{2>:D} {5<:M} {2>:h}:{02>:m}" + ).unwrap(); + + static ref OTHER_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse( + "{2>:D} {:M} {2>:h}:{02>:m}" + ).unwrap(); + + static ref FOUR_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse( + "{2>:D} {4<:M} {5>:Y}" + ).unwrap(); + + static ref FIVE_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse( + "{2>:D} {5<:M} {5>:Y}" + ).unwrap(); + + static ref OTHER_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse( + "{2>:D} {:M} {5>:Y}" + ).unwrap(); +}