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::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 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() {
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 isnt 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<GitCache> {
}
impl<'args> Exa<'args> {
pub fn from_args<I>(args: I, writer: Stdout) -> Result<Exa<'args>, Misfire>
where I: Iterator<Item = &'args OsString>
{
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<i32> {
debug!("Running with options: {:#?}", self.options);
// 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 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;

View File

@ -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<Self, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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 wouldnt 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 {
/// flags 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 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)? {
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'))));
}

View File

@ -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 programs 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 didnt 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 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.
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<glob::PatternError> for Misfire {
impl From<glob::PatternError> 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 {

View File

@ -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<Self, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
Ok(Self {
list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
reverse: matches.has(&flags::REVERSE)?,
@ -29,7 +29,7 @@ impl SortField {
/// This arguments value can be one of several flags, listed above.
/// Returns the default sort field if none is given, or `Err` if the
/// 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)? {
Some(w) => w,
None => return Ok(Self::default()),
@ -38,7 +38,7 @@ impl SortField {
// Get String because we cant 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` wont work: listing the
/// 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);
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` arguments value. This is a list of strings
/// 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
// match anything, rather than, say, `None`.
@ -204,7 +204,7 @@ impl IgnorePatterns {
impl GitIgnore {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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'))));
}

View File

@ -86,15 +86,15 @@ impl HelpString {
/// We dont do any strict-mode error checking here: its 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<Self> {
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 isnt passed
assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isnt passed
}
}

View File

@ -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<Item = &'args OsString>,
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 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 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<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::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 wasnt, and an error in
/// 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))
.map(|flag| flag.is_some())
}
@ -383,7 +383,7 @@ impl<'a> MatchedFlags<'a> {
/// argument satisfy the predicate.
///
/// 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 {
if self.is_strict() {
let all = self.flags.iter()
@ -391,7 +391,7 @@ impl<'a> MatchedFlags<'a> {
.collect::<Vec<_>>();
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 wasnt, and an error in strict mode if it was specified more
/// 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))
}
@ -417,7 +417,7 @@ impl<'a> MatchedFlags<'a> {
/// multiple arguments matched the predicate.
///
/// 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 {
if self.is_strict() {
let those = self.flags.iter()
@ -425,7 +425,7 @@ impl<'a> MatchedFlags<'a> {
.collect::<Vec<_>>();
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 },
}
// Its technically possible for ParseError::UnknownArgument to borrow its
// OsStr rather than owning it, but that would give ParseError a lifetime,
// which would give Misfire a lifetime, which gets used everywhere. And this
// only happens when an error occurs, so its not really worth it.
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

View File

@ -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<Self, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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<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 {
use crate::info::filetype::FileExtensions;
use crate::output::file_name::NoFileColours;
@ -204,7 +204,7 @@ impl ExtensionMappings {
impl Classify {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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)));

View File

@ -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 doesnt bother checking for errors.
pub fn deduce(matches: &MatchedFlags) -> Result<(), Self> {
/// Like --help, this doesnt check for errors.
pub fn deduce(matches: &MatchedFlags) -> Option<Self> {
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(_)));
}
}

View File

@ -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 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;
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<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
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: Im 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 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;
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<V: Vars>(vars: &V) -> Result<Self, Misfire> {
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
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<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> {
let env = Environment::load_all();
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
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<Self, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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<Self, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> {
pub use crate::output::time::{DefaultFormat, ISOFormat};
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
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 {
/// Its valid to show more than one column by passing in more than one
/// option, but passing *no* options means that the user just wants to
/// see the default set.
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
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));
}
}

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
/// columns for each row.
#[derive(Debug)]
#[derive(PartialEq, Debug)]
pub struct Options {
/// Options specific to drawing a table.

View File

@ -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 its harder to see whats 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

View File

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

View File

@ -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<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.
#[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<TimeZone> {
}
}
lazy_static! {
static ref ENVIRONMENT: Environment = Environment::load_all();
}
pub struct Table<'a> {
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> {
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,
}

View File

@ -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 users 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 doesnt need a locale.
ISOFormat(ISOFormat),
/// for the month so it doesnt 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 *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(),
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();
}