Replace Misfire with a testable OptionsResult

This was meant to be a small change, but it spiralled into a big one.

The original intention was to separate OptionsResult and OptionsError. With these types separated, the Help and Version variants can only be returned from the Options::parse function, and the later option-parsing functions can only return success or errors.

Also, Misfire was a silly name.

As a side-effect of Options::parse returning OptionsResult instead of Result<Options, Misfire>, we could no longer use unwrap() or unwrap_err() to get the contents out. This commit makes OptionsResult into a value type, and Options::parse a pure function. It feels like it should be one, having its return value entirely dependent on its arguments, but it also loaded locales and time zones. These parts have been moved into lazy_static references, and the code still passes tests without much change.

OptionsResult isn't PartialEq yet, because the file colouring uses a Box internally.
This commit is contained in:
Benjamin Sago 2020-10-12 23:47:36 +01:00
parent f8df02dae7
commit ed59428cbc
15 changed files with 401 additions and 447 deletions

View File

@ -13,8 +13,7 @@ use log::*;
use crate::fs::{Dir, File}; use crate::fs::{Dir, File};
use crate::fs::feature::git::GitCache; use crate::fs::feature::git::GitCache;
use crate::fs::filter::GitIgnore; use crate::fs::filter::GitIgnore;
use crate::options::{Options, Vars}; use crate::options::{Options, Vars, vars, OptionsResult};
pub use crate::options::{Misfire, vars};
use crate::output::{escape, lines, grid, grid_details, details, View, Mode}; use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
mod fs; mod fs;
@ -31,43 +30,53 @@ fn main() {
logger::configure(env::var_os(vars::EXA_DEBUG)); logger::configure(env::var_os(vars::EXA_DEBUG));
let args: Vec<_> = env::args_os().skip(1).collect(); let args: Vec<_> = env::args_os().skip(1).collect();
match Exa::from_args(args.iter(), stdout()) { match Options::parse(&args, &LiveVars) {
Ok(mut exa) => { OptionsResult::Ok(options, mut input_paths) => {
// List the current directory by default.
// (This has to be done here, otherwise git_options wont 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() { match exa.run() {
Ok(exit_status) => { 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) => { Err(e) => {
match e.kind() { eprintln!("{}", e);
ErrorKind::BrokenPipe => { exit(exits::RUNTIME_ERROR);
exit(exits::SUCCESS);
}
_ => {
eprintln!("{}", e);
exit(exits::RUNTIME_ERROR);
}
};
} }
}; }
} }
Err(ref e) if e.is_error() => { OptionsResult::Help(help_text) => {
let mut stderr = stderr(); println!("{}", help_text);
writeln!(stderr, "{}", e).unwrap(); }
if let Some(s) = e.suggestion() { OptionsResult::Version(version_str) => {
let _ = writeln!(stderr, "{}", s); println!("{}", version_str);
}
OptionsResult::InvalidOptions(error) => {
eprintln!("{}", error);
if let Some(s) = error.suggestion() {
eprintln!("{}", s);
} }
exit(exits::OPTIONS_ERROR); 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 /// List of the free command-line arguments that should correspond to file
/// names (anything that isnt an option). /// names (anything that isnt an option).
pub args: Vec<&'args OsStr>, pub input_paths: Vec<&'args OsStr>,
/// A global Git cache, if the option was passed in. /// A global Git cache, if the option was passed in.
/// This has to last the lifetime of the program, because the user might /// 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<GitCache> {
} }
impl<'args> Exa<'args> { impl<'args> Exa<'args> {
pub fn from_args<I>(args: I, writer: Stdout) -> Result<Exa<'args>, Misfire> pub fn run(mut self) -> IOResult<i32> {
where I: Iterator<Item = &'args OsString> debug!("Running with options: {:#?}", self.options);
{
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);
// List the current directory by default, like ls.
// This has to be done here, otherwise git_options wont 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<i32> {
let mut files = Vec::new(); let mut files = Vec::new();
let mut dirs = Vec::new(); let mut dirs = Vec::new();
let mut exit_status = 0; 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) { match File::from_args(PathBuf::from(file_path), None, None) {
Err(e) => { Err(e) => {
exit_status = 2; exit_status = 2;

View File

@ -1,7 +1,7 @@
//! Parsing the options for `DirAction`. //! Parsing the options for `DirAction`.
use crate::options::parser::MatchedFlags; use crate::options::parser::MatchedFlags;
use crate::options::{flags, Misfire}; use crate::options::{flags, OptionsError};
use crate::fs::dir_action::{DirAction, RecurseOptions}; use crate::fs::dir_action::{DirAction, RecurseOptions};
@ -12,7 +12,7 @@ impl DirAction {
/// There are three possible actions, and they overlap somewhat: the /// There are three possible actions, and they overlap somewhat: the
/// `--tree` flag is another form of recursion, so those two are allowed /// `--tree` flag is another form of recursion, so those two are allowed
/// to both be present, but the `--list-dirs` flag is used separately. /// to both be present, but the `--list-dirs` flag is used separately.
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let recurse = matches.has(&flags::RECURSE)?; let recurse = matches.has(&flags::RECURSE)?;
let as_file = matches.has(&flags::LIST_DIRS)?; let as_file = matches.has(&flags::LIST_DIRS)?;
let tree = matches.has(&flags::TREE)?; let tree = matches.has(&flags::TREE)?;
@ -20,13 +20,13 @@ impl DirAction {
if matches.is_strict() { if matches.is_strict() {
// Early check for --level when it wouldnt do anything // Early check for --level when it wouldnt do anything
if ! recurse && ! tree && matches.count(&flags::LEVEL) > 0 { 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 { 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 { 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 {
/// flags value, and whether the `--tree` flag was passed, which was /// flags value, and whether the `--tree` flag was passed, which was
/// determined earlier. The maximum level should be a number, and this /// determined earlier. The maximum level should be a number, and this
/// will fail with an `Err` if it isnt. /// will fail with an `Err` if it isnt.
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<Self, Misfire> { pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<Self, OptionsError> {
let max_depth = if let Some(level) = matches.get(&flags::LEVEL)? { let max_depth = if let Some(level) = matches.get(&flags::LEVEL)? {
match level.to_string_lossy().parse() { match level.to_string_lossy().parse() {
Ok(l) => Some(l), Ok(l) => Some(l),
Err(e) => return Err(Misfire::FailedParse(e)), Err(e) => return Err(OptionsError::FailedParse(e)),
} }
} }
else { else {
@ -115,12 +115,12 @@ mod test {
test!(dirs_tree: DirAction <- ["--list-dirs", "--tree"]; Last => Ok(Recurse(RecurseOptions { tree: true, max_depth: None }))); 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!(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_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(Misfire::Conflict(&flags::TREE, &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(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))); test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)));
// Overriding levels // Overriding levels
test!(overriding_1: DirAction <- ["-RL=6", "-L=7"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(7) }))); 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'))));
} }

View File

@ -2,17 +2,16 @@ use std::ffi::OsString;
use std::fmt; use std::fmt;
use std::num::ParseIntError; use std::num::ParseIntError;
use crate::options::{flags, HelpString, VersionString}; use crate::options::flags;
use crate::options::parser::{Arg, Flag, ParseError}; use crate::options::parser::{Arg, Flag, ParseError};
/// A **misfire** is a thing that can happen instead of listing files — a /// Something wrong with the combination of options the user has picked.
/// catch-all for anything outside the programs normal execution.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum Misfire { pub enum OptionsError {
/// The getopts crate didnt like these Arguments. /// There was an error (from `getopts`) parsing the arguments.
InvalidOptions(ParseError), Parse(ParseError),
/// The user supplied an illegal choice to an Argument. /// The user supplied an illegal choice to an Argument.
BadArgument(&'static Arg, OsString), BadArgument(&'static Arg, OsString),
@ -20,13 +19,6 @@ pub enum Misfire {
/// The user supplied a set of options /// The user supplied a set of options
Unsupported(String), Unsupported(String),
/// The user asked for help. This isnt strictly an error, which is why
/// this enum isnt named Error!
Help(HelpString),
/// The user wanted the version number.
Version(VersionString),
/// An option was given twice or more in strict mode. /// An option was given twice or more in strict mode.
Duplicate(Flag, Flag), Duplicate(Flag, Flag),
@ -51,21 +43,13 @@ pub enum Misfire {
FailedGlobPattern(String), FailedGlobPattern(String),
} }
impl Misfire { impl From<glob::PatternError> for OptionsError {
/// The OS return code this misfire should signify.
pub fn is_error(&self) -> bool {
! matches!(self, Self::Help(_) | Self::Version(_))
}
}
impl From<glob::PatternError> for Misfire {
fn from(error: glob::PatternError) -> Self { fn from(error: glob::PatternError) -> Self {
Self::FailedGlobPattern(error.to_string()) Self::FailedGlobPattern(error.to_string())
} }
} }
impl fmt::Display for Misfire { impl fmt::Display for OptionsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use crate::options::parser::TakesValue; use crate::options::parser::TakesValue;
@ -77,11 +61,9 @@ impl fmt::Display for Misfire {
else { else {
write!(f, "Option {} has no {:?} setting", arg, attempt) 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::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::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) if a == b => write!(f, "Flag {} was given twice", a),
Self::Duplicate(a, b) => write!(f, "Flag {} conflicts with flag {}", a, b), 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 { impl OptionsError {
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 Misfire {
/// Try to second-guess what the user was trying to do, depending on what /// Try to second-guess what the user was trying to do, depending on what
/// went wrong. /// went wrong.
pub fn suggestion(&self) -> Option<&'static str> { pub fn suggestion(&self) -> Option<&'static str> {
@ -116,7 +87,7 @@ impl Misfire {
Self::BadArgument(time, r) if *time == &flags::TIME && r == "r" => { Self::BadArgument(time, r) if *time == &flags::TIME && r == "r" => {
Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\"") 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\"") 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. /// A list of legal choices for an argument-taking option.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Choices(&'static [&'static str]); pub struct Choices(pub &'static [&'static str]);
impl fmt::Display for Choices { impl fmt::Display for Choices {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

View File

@ -3,14 +3,14 @@
use crate::fs::DotFilter; use crate::fs::DotFilter;
use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore}; use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
use crate::options::{flags, Misfire}; use crate::options::{flags, OptionsError};
use crate::options::parser::MatchedFlags; use crate::options::parser::MatchedFlags;
impl FileFilter { impl FileFilter {
/// Determines which of all the file filter options to use. /// Determines which of all the file filter options to use.
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
Ok(Self { Ok(Self {
list_dirs_first: matches.has(&flags::DIRS_FIRST)?, list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
reverse: matches.has(&flags::REVERSE)?, reverse: matches.has(&flags::REVERSE)?,
@ -29,7 +29,7 @@ impl SortField {
/// This arguments value can be one of several flags, listed above. /// This arguments value can be one of several flags, listed above.
/// Returns the default sort field if none is given, or `Err` if the /// Returns the default sort field if none is given, or `Err` if the
/// value doesnt correspond to a sort field we know about. /// value doesnt correspond to a sort field we know about.
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let word = match matches.get(&flags::SORT)? { let word = match matches.get(&flags::SORT)? {
Some(w) => w, Some(w) => w,
None => return Ok(Self::default()), None => return Ok(Self::default()),
@ -38,7 +38,7 @@ impl SortField {
// Get String because we cant match an OsStr // Get String because we cant match an OsStr
let word = match word.to_str() { let word = match word.to_str() {
Some(w) => w, 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 { let field = match word {
@ -98,7 +98,7 @@ impl SortField {
Self::Unsorted 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 /// It also checks for the `--tree` option in strict mode, because of a
/// special case where `--tree --all --all` wont work: listing the /// special case where `--tree --all --all` wont work: listing the
/// parent directory in tree mode would loop onto itself! /// parent directory in tree mode would loop onto itself!
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let count = matches.count(&flags::ALL); let count = matches.count(&flags::ALL);
if count == 0 { if count == 0 {
@ -163,10 +163,10 @@ impl DotFilter {
Ok(Self::Dotfiles) Ok(Self::Dotfiles)
} }
else if matches.count(&flags::TREE) > 0 { else if matches.count(&flags::TREE) > 0 {
Err(Misfire::TreeAllAll) Err(OptionsError::TreeAllAll)
} }
else if count >= 3 && matches.is_strict() { else if count >= 3 && matches.is_strict() {
Err(Misfire::Conflict(&flags::ALL, &flags::ALL)) Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))
} }
else { else {
Ok(Self::DotfilesAndDots) Ok(Self::DotfilesAndDots)
@ -180,7 +180,7 @@ impl IgnorePatterns {
/// Determines the set of glob patterns to use based on the /// Determines the set of glob patterns to use based on the
/// `--ignore-glob` arguments value. This is a list of strings /// `--ignore-glob` arguments value. This is a list of strings
/// separated by pipe (`|`) characters, given in any order. /// separated by pipe (`|`) characters, given in any order.
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
// If there are no inputs, we return a set of patterns that doesnt // If there are no inputs, we return a set of patterns that doesnt
// match anything, rather than, say, `None`. // match anything, rather than, say, `None`.
@ -204,7 +204,7 @@ impl IgnorePatterns {
impl GitIgnore { impl GitIgnore {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
if matches.has(&flags::GIT_IGNORE)? { if matches.has(&flags::GIT_IGNORE)? {
Ok(Self::CheckAndIgnore) Ok(Self::CheckAndIgnore)
} }
@ -260,13 +260,13 @@ mod test {
test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc))); test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc)));
// Errors // 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 // Overriding
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate)); 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_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_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(Misfire::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_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots));
test!(all_all_3: DotFilter <- ["-aaa"]; Last => 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 // --all and --tree
test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles)); test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles));
test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(Misfire::TreeAllAll)); test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll));
test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(Misfire::TreeAllAll)); test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll));
} }
@ -309,8 +309,8 @@ mod test {
// Overriding // Overriding
test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ]))); 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_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_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(Misfire::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'))));
} }

View File

@ -86,15 +86,15 @@ impl HelpString {
/// We dont do any strict-mode error checking here: its OK to give /// We dont do any strict-mode error checking here: its OK to give
/// the --help or --long flags more than once. Actually checking for /// the --help or --long flags more than once. Actually checking for
/// errors when the user wants help is kind of petty! /// errors when the user wants help is kind of petty!
pub fn deduce(matches: &MatchedFlags) -> Result<(), Self> { pub fn deduce(matches: &MatchedFlags) -> Option<Self> {
if matches.count(&flags::HELP) > 0 { if matches.count(&flags::HELP) > 0 {
let only_long = matches.count(&flags::LONG) > 0; let only_long = matches.count(&flags::LONG) > 0;
let git = cfg!(feature="git"); let git = cfg!(feature="git");
let xattrs = xattr::ENABLED; let xattrs = xattr::ENABLED;
Err(Self { only_long, git, xattrs }) Some(Self { only_long, git, xattrs })
} }
else { else {
Ok(()) // no help needs to be shown None
} }
} }
} }
@ -129,7 +129,7 @@ impl fmt::Display for HelpString {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::options::Options; use crate::options::{Options, OptionsResult};
use std::ffi::OsString; use std::ffi::OsString;
fn os(input: &'static str) -> OsString { fn os(input: &'static str) -> OsString {
@ -142,20 +142,20 @@ mod test {
fn help() { fn help() {
let args = [ os("--help") ]; let args = [ os("--help") ];
let opts = Options::parse(&args, &None); let opts = Options::parse(&args, &None);
assert!(opts.is_err()) assert!(matches!(opts, OptionsResult::Help(_)));
} }
#[test] #[test]
fn help_with_file() { fn help_with_file() {
let args = [ os("--help"), os("me") ]; let args = [ os("--help"), os("me") ];
let opts = Options::parse(&args, &None); let opts = Options::parse(&args, &None);
assert!(opts.is_err()) assert!(matches!(opts, OptionsResult::Help(_)));
} }
#[test] #[test]
fn unhelpful() { fn unhelpful() {
let args = []; let args = [];
let opts = Options::parse(&args, &None); let opts = Options::parse(&args, &None);
assert!(opts.is_ok()) // no help when --help isnt passed assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isnt passed
} }
} }

View File

@ -81,20 +81,20 @@ mod flags;
mod style; mod style;
mod view; mod view;
mod error;
pub use self::error::OptionsError;
mod help; mod help;
use self::help::HelpString; use self::help::HelpString;
mod version; mod parser;
use self::version::VersionString; use self::parser::MatchedFlags;
mod misfire;
pub use self::misfire::Misfire;
pub mod vars; pub mod vars;
pub use self::vars::Vars; pub use self::vars::Vars;
mod parser; mod version;
use self::parser::MatchedFlags; use self::version::VersionString;
/// These **options** represent a parsed, error-checked versions of the /// 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 /// struct and a list of free filenames, using the environment variables
/// for extra options. /// for extra options.
#[allow(unused_results)] #[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<Item = &'args OsString>, where I: IntoIterator<Item = &'args OsString>,
V: Vars V: Vars,
{ {
use crate::options::parser::{Matches, Strictness}; use crate::options::parser::{Matches, Strictness};
@ -132,15 +132,22 @@ impl Options {
}; };
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) { let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) {
Ok(m) => m, Ok(m) => m,
Err(e) => return Err(Misfire::InvalidOptions(e)), Err(pe) => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)),
}; };
HelpString::deduce(&flags).map_err(Misfire::Help)?; if let Some(help) = HelpString::deduce(&flags) {
VersionString::deduce(&flags).map_err(Misfire::Version)?; return OptionsResult::Help(help);
}
let options = Self::deduce(&flags, vars)?; if let Some(version) = VersionString::deduce(&flags) {
Ok((options, frees)) 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 /// 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 /// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed. /// arguments, after theyve been parsed.
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> { fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let dir_action = DirAction::deduce(matches)?; let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?; let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches, vars)?; 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)] #[cfg(test)]
pub mod test { pub mod test {
use super::{Options, Misfire, flags};
use crate::options::parser::{Arg, MatchedFlags}; use crate::options::parser::{Arg, MatchedFlags};
use std::ffi::OsString; use std::ffi::OsString;
@ -218,32 +242,4 @@ pub mod test {
os.push(input); os.push(input);
os 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<OsString> = Vec::new();
let outs = Options::parse(&nothing, &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))
}
} }

View File

@ -31,7 +31,7 @@
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::fmt; use std::fmt;
use crate::options::Misfire; use crate::options::error::{OptionsError, Choices};
/// A **short argument** is a single ASCII character. /// A **short argument** is a single ASCII character.
@ -373,7 +373,7 @@ impl<'a> MatchedFlags<'a> {
/// Whether the given argument was specified. /// Whether the given argument was specified.
/// Returns `true` if it was, `false` if it wasnt, and an error in /// Returns `true` if it was, `false` if it wasnt, and an error in
/// strict mode if it was specified more than once. /// strict mode if it was specified more than once.
pub fn has(&self, arg: &'static Arg) -> Result<bool, Misfire> { pub fn has(&self, arg: &'static Arg) -> Result<bool, OptionsError> {
self.has_where(|flag| flag.matches(arg)) self.has_where(|flag| flag.matches(arg))
.map(|flag| flag.is_some()) .map(|flag| flag.is_some())
} }
@ -383,7 +383,7 @@ impl<'a> MatchedFlags<'a> {
/// argument satisfy the predicate. /// argument satisfy the predicate.
/// ///
/// Youll have to test the resulting flag to see which argument it was. /// Youll have to test the resulting flag to see which argument it was.
pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, Misfire> pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, OptionsError>
where P: Fn(&Flag) -> bool { where P: Fn(&Flag) -> bool {
if self.is_strict() { if self.is_strict() {
let all = self.flags.iter() let all = self.flags.iter()
@ -391,7 +391,7 @@ impl<'a> MatchedFlags<'a> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if all.len() < 2 { Ok(all.first().map(|t| &t.0)) } 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 { else {
let any = self.flags.iter().rev() 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 /// Returns the value of the given argument if it was specified, nothing
/// if it wasnt, and an error in strict mode if it was specified more /// if it wasnt, and an error in strict mode if it was specified more
/// than once. /// than once.
pub fn get(&self, arg: &'static Arg) -> Result<Option<&OsStr>, Misfire> { pub fn get(&self, arg: &'static Arg) -> Result<Option<&OsStr>, OptionsError> {
self.get_where(|flag| flag.matches(arg)) self.get_where(|flag| flag.matches(arg))
} }
@ -417,7 +417,7 @@ impl<'a> MatchedFlags<'a> {
/// multiple arguments matched the predicate. /// multiple arguments matched the predicate.
/// ///
/// Its not possible to tell which flag the value belonged to from this. /// Its not possible to tell which flag the value belonged to from this.
pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, Misfire> pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, OptionsError>
where P: Fn(&Flag) -> bool { where P: Fn(&Flag) -> bool {
if self.is_strict() { if self.is_strict() {
let those = self.flags.iter() let those = self.flags.iter()
@ -425,7 +425,7 @@ impl<'a> MatchedFlags<'a> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) } 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 { else {
let found = self.flags.iter().rev() let found = self.flags.iter().rev()
@ -475,10 +475,17 @@ pub enum ParseError {
UnknownArgument { attempt: OsString }, UnknownArgument { attempt: OsString },
} }
// Its technically possible for ParseError::UnknownArgument to borrow its impl fmt::Display for ParseError {
// OsStr rather than owning it, but that would give ParseError a lifetime, fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// which would give Misfire a lifetime, which gets used everywhere. And this match self {
// only happens when an error occurs, so its not really worth it. 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 /// Splits a string on its `=` character, returning the two substrings on

View File

@ -1,7 +1,7 @@
use ansi_term::Style; use ansi_term::Style;
use crate::fs::File; use crate::fs::File;
use crate::options::{flags, Vars, Misfire}; use crate::options::{flags, Vars, OptionsError};
use crate::options::parser::MatchedFlags; use crate::options::parser::MatchedFlags;
use crate::output::file_name::{Classify, FileStyle}; use crate::output::file_name::{Classify, FileStyle};
use crate::style::Colours; use crate::style::Colours;
@ -37,7 +37,7 @@ impl Default for TerminalColours {
impl TerminalColours { impl TerminalColours {
/// Determine which terminal colour conditions to use. /// Determine which terminal colour conditions to use.
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? { let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
Some(w) => w, Some(w) => w,
None => return Ok(Self::default()), None => return Ok(Self::default()),
@ -53,7 +53,7 @@ impl TerminalColours {
Ok(Self::Never) Ok(Self::Never)
} }
else { else {
Err(Misfire::BadArgument(&flags::COLOR, word.into())) Err(OptionsError::BadArgument(&flags::COLOR, word.into()))
} }
} }
} }
@ -77,7 +77,7 @@ pub struct Styles {
impl Styles { impl Styles {
#[allow(trivial_casts)] // the `as Box<_>` stuff below warns about this for some reason #[allow(trivial_casts)] // the `as Box<_>` stuff below warns about this for some reason
pub fn deduce<V, TW>(matches: &MatchedFlags, vars: &V, widther: TW) -> Result<Self, Misfire> pub fn deduce<V, TW>(matches: &MatchedFlags, vars: &V, widther: TW) -> Result<Self, OptionsError>
where TW: Fn() -> Option<usize>, V: Vars { where TW: Fn() -> Option<usize>, V: Vars {
use crate::info::filetype::FileExtensions; use crate::info::filetype::FileExtensions;
use crate::output::file_name::NoFileColours; use crate::output::file_name::NoFileColours;
@ -204,7 +204,7 @@ impl ExtensionMappings {
impl Classify { impl Classify {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let flagged = matches.has(&flags::CLASSIFY)?; let flagged = matches.has(&flags::CLASSIFY)?;
if flagged { Ok(Self::AddFileIndicators) } if flagged { Ok(Self::AddFileIndicators) }
@ -260,8 +260,8 @@ mod terminal_test {
test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never)); test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never));
// Errors // Errors
test!(no_u_error: ["--color=upstream"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color 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 Misfire::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one! test!(u_error: ["--colour=lovers"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one!
// Overriding // Overriding
test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never)); 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_3: ["--colour=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_4: ["--color=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_5: ["--colour=auto", "--colour=never"]; Complain => err OptionsError::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_6: ["--color=auto", "--colour=never"]; Complain => err OptionsError::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_7: ["--colour=auto", "--color=never"]; Complain => err OptionsError::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_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_3: ["--color=always", "--colour-scale"], || None; Last => Ok(Colours::colourful(true)));
test!(scale_4: ["--color=always", ], || None; Last => Ok(Colours::colourful(false))); 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_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_7: ["--color=always", "--colour-scale"], || None; Complain => Ok(Colours::colourful(true)));
test!(scale_8: ["--color=always", ], || None; Complain => Ok(Colours::colourful(false))); test!(scale_8: ["--color=always", ], || None; Complain => Ok(Colours::colourful(false)));

View File

@ -18,13 +18,13 @@ impl VersionString {
/// command-line arguments. This one works backwards from the other /// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown. /// deduce functions, returning Err if help needs to be shown.
/// ///
/// Like --help, this doesnt bother checking for errors. /// Like --help, this doesnt check for errors.
pub fn deduce(matches: &MatchedFlags) -> Result<(), Self> { pub fn deduce(matches: &MatchedFlags) -> Option<Self> {
if matches.count(&flags::VERSION) > 0 { if matches.count(&flags::VERSION) > 0 {
Err(Self) Some(Self)
} }
else { else {
Ok(()) // no version needs to be shown None
} }
} }
} }
@ -38,7 +38,7 @@ impl fmt::Display for VersionString {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::options::Options; use crate::options::{Options, OptionsResult};
use std::ffi::OsString; use std::ffi::OsString;
fn os(input: &'static str) -> OsString { fn os(input: &'static str) -> OsString {
@ -48,9 +48,16 @@ mod test {
} }
#[test] #[test]
fn help() { fn version() {
let args = [ os("--version") ]; let args = [ os("--version") ];
let opts = Options::parse(&args, &None); 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(_)));
} }
} }

View File

@ -1,18 +1,18 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::fs::feature::xattr; use crate::fs::feature::xattr;
use crate::options::{flags, Misfire, Vars}; use crate::options::{flags, OptionsError, Vars};
use crate::options::parser::MatchedFlags; use crate::options::parser::MatchedFlags;
use crate::output::{View, Mode, grid, details, lines}; use crate::output::{View, Mode, grid, details, lines};
use crate::output::grid_details::{self, RowThreshold}; 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; use crate::output::time::TimeFormat;
impl View { impl View {
/// Determine which view to use and all of that views arguments. /// Determine which view to use and all of that views arguments.
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> { pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
use crate::options::style::Styles; use crate::options::style::Styles;
let mode = Mode::deduce(matches, vars)?; let mode = Mode::deduce(matches, vars)?;
@ -25,13 +25,13 @@ impl View {
impl Mode { impl Mode {
/// Determine the mode from the command-line arguments. /// Determine the mode from the command-line arguments.
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> { pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let long = || { let long = || {
if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? { 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)? { 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 { else {
Ok(details::Options { Ok(details::Options {
@ -47,7 +47,7 @@ impl Mode {
if let Some(width) = TerminalWidth::deduce(vars)?.width() { if let Some(width) = TerminalWidth::deduce(vars)?.width() {
if matches.has(&flags::ONE_LINE)? { if matches.has(&flags::ONE_LINE)? {
if matches.has(&flags::ACROSS)? { if matches.has(&flags::ACROSS)? {
Err(Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE)) Err(OptionsError::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
} }
else { else {
let lines = lines::Options { icons: matches.has(&flags::ICONS)? }; 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, for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] { &flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
if matches.has(option)? { 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)? { 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)? { else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? {
// TODO: Im not sure if the code even gets this far. // TODO: Im not sure if the code even gets this far.
// There is an identical check in dir_action // 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. /// Determine a requested terminal width from the command-line arguments.
/// ///
/// Returns an error if a requested width doesnt parse to an integer. /// Returns an error if a requested width doesnt parse to an integer.
fn deduce<V: Vars>(vars: &V) -> Result<Self, Misfire> { fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars; use crate::options::vars;
if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) { if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) {
match columns.parse() { match columns.parse() {
Ok(width) => Ok(Self::Set(width)), 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 { else if let Some(width) = *TERM_WIDTH {
@ -190,13 +190,13 @@ impl RowThreshold {
/// Determine whether to use a row threshold based on the given /// Determine whether to use a row threshold based on the given
/// environment variables. /// environment variables.
fn deduce<V: Vars>(vars: &V) -> Result<Self, Misfire> { fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars; use crate::options::vars;
if let Some(columns) = vars.get(vars::EXA_GRID_ROWS).and_then(|s| s.into_string().ok()) { if let Some(columns) = vars.get(vars::EXA_GRID_ROWS).and_then(|s| s.into_string().ok()) {
match columns.parse() { match columns.parse() {
Ok(rows) => Ok(Self::MinimumRows(rows)), Ok(rows) => Ok(Self::MinimumRows(rows)),
Err(e) => Err(Misfire::FailedParse(e)), Err(e) => Err(OptionsError::FailedParse(e)),
} }
} }
else { else {
@ -207,18 +207,17 @@ impl RowThreshold {
impl TableOptions { impl TableOptions {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> { fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let env = Environment::load_all();
let time_format = TimeFormat::deduce(matches, vars)?; let time_format = TimeFormat::deduce(matches, vars)?;
let size_format = SizeFormat::deduce(matches)?; let size_format = SizeFormat::deduce(matches)?;
let columns = Columns::deduce(matches)?; let columns = Columns::deduce(matches)?;
Ok(Self { env, time_format, size_format, columns }) Ok(Self { time_format, size_format, columns })
} }
} }
impl Columns { impl Columns {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let time_types = TimeTypes::deduce(matches)?; let time_types = TimeTypes::deduce(matches)?;
let git = cfg!(feature = "git") && matches.has(&flags::GIT)?; 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 /// strings of digits in your head. Changing the format to anything else
/// involves the `--binary` or `--bytes` flags, and these conflict with /// involves the `--binary` or `--bytes` flags, and these conflict with
/// each other. /// each other.
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?; let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?;
Ok(match flag { Ok(match flag {
@ -262,9 +261,7 @@ impl SizeFormat {
impl TimeFormat { impl TimeFormat {
/// Determine how time should be formatted in timestamp columns. /// Determine how time should be formatted in timestamp columns.
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> { fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
pub use crate::output::time::{DefaultFormat, ISOFormat};
let word = match matches.get(&flags::TIME_STYLE)? { let word = match matches.get(&flags::TIME_STYLE)? {
Some(w) => { Some(w) => {
w.to_os_string() w.to_os_string()
@ -273,16 +270,16 @@ impl TimeFormat {
use crate::options::vars; use crate::options::vars;
match vars.get(vars::TIME_STYLE) { match vars.get(vars::TIME_STYLE) {
Some(ref t) if ! t.is_empty() => t.clone(), Some(ref t) if ! t.is_empty() => t.clone(),
_ => return Ok(Self::DefaultFormat(DefaultFormat::load())) _ => return Ok(Self::DefaultFormat)
} }
}, },
}; };
if &word == "default" { if &word == "default" {
Ok(Self::DefaultFormat(DefaultFormat::load())) Ok(Self::DefaultFormat)
} }
else if &word == "iso" { else if &word == "iso" {
Ok(Self::ISOFormat(ISOFormat::load())) Ok(Self::ISOFormat)
} }
else if &word == "long-iso" { else if &word == "long-iso" {
Ok(Self::LongISO) Ok(Self::LongISO)
@ -291,7 +288,7 @@ impl TimeFormat {
Ok(Self::FullISO) Ok(Self::FullISO)
} }
else { else {
Err(Misfire::BadArgument(&flags::TIME_STYLE, word)) Err(OptionsError::BadArgument(&flags::TIME_STYLE, word))
} }
} }
} }
@ -309,7 +306,7 @@ impl TimeTypes {
/// Its valid to show more than one column by passing in more than one /// Its valid to show more than one column by passing in more than one
/// option, but passing *no* options means that the user just wants to /// option, but passing *no* options means that the user just wants to
/// see the default set. /// see the default set.
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let possible_word = matches.get(&flags::TIME)?; let possible_word = matches.get(&flags::TIME)?;
let modified = matches.has(&flags::MODIFIED)?; let modified = matches.has(&flags::MODIFIED)?;
let changed = matches.has(&flags::CHANGED)?; let changed = matches.has(&flags::CHANGED)?;
@ -322,16 +319,16 @@ impl TimeTypes {
Self { modified: false, changed: false, accessed: false, created: false } Self { modified: false, changed: false, accessed: false, created: false }
} else if let Some(word) = possible_word { } else if let Some(word) = possible_word {
if modified { if modified {
return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME)); return Err(OptionsError::Useless(&flags::MODIFIED, true, &flags::TIME));
} }
else if changed { else if changed {
return Err(Misfire::Useless(&flags::CHANGED, true, &flags::TIME)); return Err(OptionsError::Useless(&flags::CHANGED, true, &flags::TIME));
} }
else if accessed { else if accessed {
return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME)); return Err(OptionsError::Useless(&flags::ACCESSED, true, &flags::TIME));
} }
else if created { 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" { else if word == "mod" || word == "modified" {
Self { modified: true, changed: false, accessed: false, created: false } Self { modified: true, changed: false, accessed: false, created: false }
@ -346,7 +343,7 @@ impl TimeTypes {
Self { modified: false, changed: false, accessed: false, created: true } Self { modified: false, changed: false, accessed: false, created: true }
} }
else { else {
return Err(Misfire::BadArgument(&flags::TIME, word.into())); return Err(OptionsError::BadArgument(&flags::TIME, word.into()));
} }
} }
else if modified || changed || accessed || created { else if modified || changed || accessed || created {
@ -474,10 +471,10 @@ mod test {
test!(both_3: SizeFormat <- ["--binary", "--bytes"]; Last => Ok(SizeFormat::JustBytes)); test!(both_3: SizeFormat <- ["--binary", "--bytes"]; Last => Ok(SizeFormat::JustBytes));
test!(both_4: SizeFormat <- ["--bytes", "--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_5: SizeFormat <- ["--binary", "--binary"]; Complain => err OptionsError::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_6: SizeFormat <- ["--bytes", "--binary"]; Complain => err OptionsError::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_7: SizeFormat <- ["--binary", "--bytes"]; Complain => err OptionsError::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_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("bytes")));
} }
@ -488,23 +485,23 @@ mod test {
// implement PartialEq. // implement PartialEq.
// Default behaviour // Default behaviour
test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat(_))); test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat));
// Individual settings // Individual settings
test!(default: TimeFormat <- ["--time-style=default"], None; Both => like Ok(TimeFormat::DefaultFormat(_))); 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!(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!(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)); test!(full_iso: TimeFormat <- ["--time-style", "full-iso"], None; Both => like Ok(TimeFormat::FullISO));
// Overriding // Overriding
test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Last => like Ok(TimeFormat::ISOFormat(_))); 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!(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!(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 // 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. // `TIME_STYLE` environment variable is defined.
// If the time-style argument is not given, `TIME_STYLE` is used. // If the time-style argument is not given, `TIME_STYLE` is used.
@ -552,12 +549,12 @@ mod test {
// Errors // Errors
test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("tea"))); test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("tea")));
test!(t_ea: TimeTypes <- ["-tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("ea"))); test!(t_ea: TimeTypes <- ["-tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("ea")));
// Overriding // Overriding
test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); 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")] #[cfg(feature = "git")]
test!(just_git: Mode <- ["--git"], None; Last => like Ok(Mode::Grid(_))); 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_header_2: Mode <- ["--header"], None; Complain => err OptionsError::Useless(&flags::HEADER, false, &flags::LONG));
test!(just_group_2: Mode <- ["--group"], None; Complain => err Misfire::Useless(&flags::GROUP, 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 Misfire::Useless(&flags::INODE, 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 Misfire::Useless(&flags::LINKS, 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 Misfire::Useless(&flags::BLOCKS, 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 Misfire::Useless(&flags::BINARY, 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 Misfire::Useless(&flags::BYTES, false, &flags::LONG)); test!(just_bytes_2: Mode <- ["--bytes"], None; Complain => err OptionsError::Useless(&flags::BYTES, false, &flags::LONG));
#[cfg(feature = "git")] #[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));
} }
} }

View File

@ -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 /// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row. /// columns for each row.
#[derive(Debug)] #[derive(PartialEq, Debug)]
pub struct Options { pub struct Options {
/// Options specific to drawing a table. /// Options specific to drawing a table.

View File

@ -19,7 +19,7 @@ use crate::output::tree::{TreeParams, TreeDepth};
use crate::style::Colours; use crate::style::Colours;
#[derive(Debug)] #[derive(PartialEq, Debug)]
pub struct Options { pub struct Options {
pub grid: GridOptions, pub grid: GridOptions,
pub details: DetailsOptions, pub details: DetailsOptions,
@ -33,7 +33,7 @@ pub struct Options {
/// small directory of four files in four columns, the files just look spaced /// small directory of four files in four columns, the files just look spaced
/// out and its harder to see whats going on. So it can be enabled just for /// out and its harder to see whats going on. So it can be enabled just for
/// larger directory listings. /// larger directory listings.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(PartialEq, Debug, Copy, Clone)]
pub enum RowThreshold { pub enum RowThreshold {
/// Only use grid-details view if it would result in at least this many /// Only use grid-details view if it would result in at least this many

View File

@ -29,7 +29,7 @@ pub struct View {
/// The **mode** is the “type” of output. /// The **mode** is the “type” of output.
#[derive(Debug)] #[derive(PartialEq, Debug)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum Mode { pub enum Mode {
Grid(grid::Options), Grid(grid::Options),

View File

@ -1,14 +1,13 @@
use std::cmp::max; use std::cmp::max;
use std::env; use std::env;
use std::fmt;
use std::ops::Deref; use std::ops::Deref;
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
use datetime::TimeZone; use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult}; use zoneinfo_compiled::{CompiledData, Result as TZResult};
use lazy_static::lazy_static;
use log::*; use log::*;
use users::UsersCache; use users::UsersCache;
use crate::fs::{File, fields as f}; use crate::fs::{File, fields as f};
@ -20,21 +19,13 @@ use crate::style::Colours;
/// Options for displaying a table. /// Options for displaying a table.
#[derive(PartialEq, Debug)]
pub struct Options { pub struct Options {
pub env: Environment,
pub size_format: SizeFormat, pub size_format: SizeFormat,
pub time_format: TimeFormat, pub time_format: TimeFormat,
pub columns: Columns, pub columns: Columns,
} }
// I had to make other types derive Debug,
// and Mutex<UsersCache> 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. /// Extra columns to display in the table.
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub struct Columns { pub struct Columns {
@ -278,7 +269,7 @@ impl Environment {
self.users.lock().unwrap() self.users.lock().unwrap()
} }
pub fn load_all() -> Self { fn load_all() -> Self {
let tz = match determine_time_zone() { let tz = match determine_time_zone() {
Ok(t) => { Ok(t) => {
Some(t) Some(t)
@ -307,6 +298,10 @@ fn determine_time_zone() -> TZResult<TimeZone> {
} }
} }
lazy_static! {
static ref ENVIRONMENT: Environment = Environment::load_all();
}
pub struct Table<'a> { pub struct Table<'a> {
columns: Vec<Column>, columns: Vec<Column>,
@ -327,13 +322,14 @@ impl<'a, 'f> Table<'a> {
pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> 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 columns = options.columns.collect(git.is_some());
let widths = TableWidths::zero(columns.len()); let widths = TableWidths::zero(columns.len());
let env = &*ENVIRONMENT;
Table { Table {
colours, colours,
widths, widths,
columns, columns,
git, git,
env: &options.env, env,
time_format: &options.time_format, time_format: &options.time_format,
size_format: options.size_format, size_format: options.size_format,
} }

View File

@ -2,9 +2,11 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece}; use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece, Month};
use datetime::fmt::DateFormat; 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**. /// 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 /// Currently exa does not support *custom* styles, where the user enters a
/// format string in an environment variable or something. Just these four. /// format string in an environment variable or something. Just these four.
#[derive(Debug)] #[derive(PartialEq, Debug)]
pub enum TimeFormat { pub enum TimeFormat {
/// The **default format** uses the users locale to print month names, /// The **default format** uses the users locale to print month names,
/// and specifies the timestamp down to the minute for recent times, and /// and specifies the timestamp down to the minute for recent times, and
/// day for older times. /// day for older times.
DefaultFormat(DefaultFormat), DefaultFormat,
/// Use the **ISO format**, which specifies the timestamp down to the /// Use the **ISO format**, which specifies the timestamp down to the
/// minute for recent times, and day for older times. It uses a number /// minute for recent times, and day for older times. It uses a number
/// for the month so it doesnt need a locale. /// for the month so it doesnt use the locale.
ISOFormat(ISOFormat), ISOFormat,
/// Use the **long ISO format**, which specifies the timestamp down to the /// Use the **long ISO format**, which specifies the timestamp down to the
/// minute using only numbers, without needing the locale or year. /// minute using only numbers, without needing the locale or year.
@ -52,150 +54,62 @@ pub enum TimeFormat {
impl TimeFormat { impl TimeFormat {
pub fn format_local(&self, time: SystemTime) -> String { pub fn format_local(&self, time: SystemTime) -> String {
match self { match self {
Self::DefaultFormat(fmt) => fmt.format_local(time), Self::DefaultFormat => default_local(time),
Self::ISOFormat(iso) => iso.format_local(time), Self::ISOFormat => iso_local(time),
Self::LongISO => long_local(time), Self::LongISO => long_local(time),
Self::FullISO => full_local(time), Self::FullISO => full_local(time),
} }
} }
pub fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String { pub fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String {
match self { match self {
Self::DefaultFormat(fmt) => fmt.format_zoned(time, zone), Self::DefaultFormat => default_zoned(time, zone),
Self::ISOFormat(iso) => iso.format_zoned(time, zone), Self::ISOFormat => iso_zoned(time, zone),
Self::LongISO => long_zoned(time, zone), Self::LongISO => long_zoned(time, zone),
Self::FullISO => full_zoned(time, zone), Self::FullISO => full_zoned(time, zone),
} }
} }
} }
#[derive(Debug, Clone)] #[allow(trivial_numeric_casts)]
pub struct DefaultFormat { 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 if date.year() == *CURRENT_YEAR {
/// format to use. format!("{:2} {} {:02}:{:02}",
pub current_year: i64, date.day(), month_to_abbrev(date.month()),
date.hour(), date.minute())
/// Localisation rules for formatting timestamps. }
pub locale: locale::Time, else {
let date_format = match *MAXIMUM_MONTH_WIDTH {
/// Date format for printing out timestamps that are in the current year. 4 => &*FOUR_WIDE_DATE_TIME,
pub date_and_time: DateFormat<'static>, 5 => &*FIVE_WIDE_DATE_TIME,
_ => &*OTHER_WIDE_DATE_TIME,
/// Date format for printing out timestamps that *arent*.
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(),
}; };
let date_and_year = match maximum_month_width { date_format.format(&date, &*LOCALE)
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()
#[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)] #[allow(trivial_numeric_casts)]
@ -235,54 +149,127 @@ fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
offset.hours(), offset.minutes().abs()) 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)] if is_recent(date) {
pub struct ISOFormat { format!("{:02}-{:02} {:02}:{:02}",
date.month() as usize, date.day(),
/// The year of the current time. This gets used to determine which date date.hour(), date.minute())
/// format to use. }
pub current_year: i64, else {
} format!("{:04}-{:02}-{:02}",
date.year(), date.month() as usize, date.day())
impl ISOFormat {
pub fn load() -> Self {
let current_year = LocalDateTime::now().year();
Self { current_year }
} }
} }
impl ISOFormat { #[allow(trivial_numeric_casts)]
fn is_recent(self, date: LocalDateTime) -> bool { fn iso_zoned(time: SystemTime, zone: &TimeZone) -> String {
date.year() == self.current_year 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())
} }
else {
#[allow(trivial_numeric_casts)] format!("{:04}-{:02}-{:02}",
fn format_local(self, time: SystemTime) -> String { date.year(), date.month() as usize, date.day())
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())
}
} }
} }
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();
}