Merge branch 'strict-mode-properly'

This adds a check for the EXA_STRICT environment variable, and uses it to put exa in a strict mode, which enables more checks for useless/redundant arguments.

Its other goal was to move all the option parsing tests out of options/mod.rs and into the files where they’re actually relevant. THIS IS NOT YET DONE and I’ll have to do some more work on this prior to release to clear up the last few lingering issues. There are almost certainly going to be cases where redundant arguments are still complained about (or ignored) by mistake.

But I want to get this branch merged so I can take care of some other stuff for the next release.
This commit is contained in:
Benjamin Sago 2017-08-11 09:58:36 +01:00
commit 97d1472331
19 changed files with 789 additions and 312 deletions

View File

@ -22,6 +22,7 @@ extern crate term_size;
extern crate lazy_static;
use std::env::var_os;
use std::ffi::{OsStr, OsString};
use std::io::{stderr, Write, Result as IOResult};
use std::path::{Component, PathBuf};
@ -29,7 +30,7 @@ use std::path::{Component, PathBuf};
use ansi_term::{ANSIStrings, Style};
use fs::{Dir, File};
use options::Options;
use options::{Options, Vars};
pub use options::Misfire;
use output::{escape, lines, grid, grid_details, details, View, Mode};
@ -55,10 +56,20 @@ pub struct Exa<'args, 'w, W: Write + 'w> {
pub args: Vec<&'args OsStr>,
}
/// The “real” environment variables type.
/// Instead of just calling `var_os` from within the options module,
/// the method of looking up environment variables has to be passed in.
struct LiveVars;
impl Vars for LiveVars {
fn get(&self, name: &'static str) -> Option<OsString> {
var_os(name)
}
}
impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
where I: Iterator<Item=&'args OsString> {
Options::getopts(args).map(move |(options, args)| {
Options::parse(args, LiveVars).map(move |(options, args)| {
Exa { options, writer, args }
})
}

View File

@ -8,12 +8,12 @@ impl DirAction {
/// Determine which action to perform when trying to list a directory.
pub fn deduce(matches: &MatchedFlags) -> Result<DirAction, Misfire> {
let recurse = matches.has(&flags::RECURSE);
let list = matches.has(&flags::LIST_DIRS);
let tree = matches.has(&flags::TREE);
let recurse = matches.has(&flags::RECURSE)?;
let list = matches.has(&flags::LIST_DIRS)?;
let tree = matches.has(&flags::TREE)?;
// Early check for --level when it wouldnt do anything
if !recurse && !tree && matches.get(&flags::LEVEL).is_some() {
if !recurse && !tree && matches.get(&flags::LEVEL)?.is_some() {
return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
}
@ -37,7 +37,7 @@ impl RecurseOptions {
/// Determine which files should be recursed into.
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<RecurseOptions, Misfire> {
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() {
Ok(l) => Some(l),
Err(e) => return Err(Misfire::FailedParse(e)),
@ -55,51 +55,50 @@ impl RecurseOptions {
#[cfg(test)]
mod test {
use super::*;
use std::ffi::OsString;
use options::flags;
pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
use options::parser::Flag;
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
#[test]
fn $name() {
use options::parser::{Args, Arg};
use std::ffi::OsString;
use options::parser::Arg;
use options::test::parse_for_test;
use options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[ &flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ];
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let results = Args(TEST_ARGS).parse(bits.iter());
assert_eq!($type::deduce(&results.unwrap().flags), $result);
static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
assert_eq!(result, $result);
}
}
};
}
// Default behaviour
test!(empty: DirAction <- [] => Ok(DirAction::List));
test!(empty: DirAction <- []; Both => Ok(DirAction::List));
// Listing files as directories
test!(dirs_short: DirAction <- ["-d"] => Ok(DirAction::AsFile));
test!(dirs_long: DirAction <- ["--list-dirs"] => Ok(DirAction::AsFile));
test!(dirs_short: DirAction <- ["-d"]; Both => Ok(DirAction::AsFile));
test!(dirs_long: DirAction <- ["--list-dirs"]; Both => Ok(DirAction::AsFile));
// Recursing
test!(rec_short: DirAction <- ["-R"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: None })));
test!(rec_long: DirAction <- ["--recurse"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: None })));
test!(rec_lim_short: DirAction <- ["-RL4"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(4) })));
test!(rec_lim_short_2: DirAction <- ["-RL=5"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(5) })));
test!(rec_lim_long: DirAction <- ["--recurse", "--level", "666"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(666) })));
test!(rec_lim_long_2: DirAction <- ["--recurse", "--level=0118"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(118) })));
test!(rec_tree: DirAction <- ["--recurse", "--tree"] => Ok(DirAction::Recurse(RecurseOptions { tree: true, max_depth: None })));
test!(rec_short_tree: DirAction <- ["--tree", "--recurse"] => Ok(DirAction::Recurse(RecurseOptions { tree: true, max_depth: None })));
use self::DirAction::Recurse;
test!(rec_short: DirAction <- ["-R"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None })));
test!(rec_long: DirAction <- ["--recurse"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None })));
test!(rec_lim_short: DirAction <- ["-RL4"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(4) })));
test!(rec_lim_short_2: DirAction <- ["-RL=5"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(5) })));
test!(rec_lim_long: DirAction <- ["--recurse", "--level", "666"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(666) })));
test!(rec_lim_long_2: DirAction <- ["--recurse", "--level=0118"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(118) })));
test!(rec_tree: DirAction <- ["--recurse", "--tree"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None })));
test!(rec_short_tree: DirAction <- ["--tree", "--recurse"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None })));
// Errors
test!(error: DirAction <- ["--list-dirs", "--recurse"] => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)));
test!(error_2: DirAction <- ["--list-dirs", "--tree"] => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS)));
test!(underwaterlevel: DirAction <- ["--level=4"] => Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)));
test!(error: DirAction <- ["--list-dirs", "--recurse"]; Both => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)));
test!(error_2: DirAction <- ["--list-dirs", "--tree"]; Both => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS)));
test!(underwaterlevel: DirAction <- ["--level=4"]; Both => Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)));
// Overriding
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'))));
}

View File

@ -11,8 +11,8 @@ impl FileFilter {
/// command-line arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> {
Ok(FileFilter {
list_dirs_first: matches.has(&flags::DIRS_FIRST),
reverse: matches.has(&flags::REVERSE),
list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
reverse: matches.has(&flags::REVERSE)?,
sort_field: SortField::deduce(matches)?,
dot_filter: DotFilter::deduce(matches)?,
ignore_patterns: IgnorePatterns::deduce(matches)?,
@ -38,7 +38,7 @@ impl SortField {
/// argument. This will return `Err` if the option is there, but does not
/// correspond to a valid field.
fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
let word = match matches.get(&flags::SORT) {
let word = match matches.get(&flags::SORT)? {
Some(w) => w,
None => return Ok(SortField::default()),
};
@ -85,11 +85,22 @@ impl SortField {
impl DotFilter {
pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> {
match matches.count(&flags::ALL) {
0 => Ok(DotFilter::JustFiles),
1 => Ok(DotFilter::Dotfiles),
_ => if matches.has(&flags::TREE) { Err(Misfire::TreeAllAll) }
else { Ok(DotFilter::DotfilesAndDots) }
let count = matches.count(&flags::ALL);
if count == 0 {
Ok(DotFilter::JustFiles)
}
else if count == 1 {
Ok(DotFilter::Dotfiles)
}
else if matches.count(&flags::TREE) > 0 {
Err(Misfire::TreeAllAll)
}
else if count >= 3 && matches.is_strict() {
Err(Misfire::Conflict(&flags::ALL, &flags::ALL))
}
else {
Ok(DotFilter::DotfilesAndDots)
}
}
}
@ -101,7 +112,7 @@ impl IgnorePatterns {
/// command-line arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> {
let inputs = match matches.get(&flags::IGNORE_GLOB) {
let inputs = match matches.get(&flags::IGNORE_GLOB)? {
None => return Ok(IgnorePatterns::empty()),
Some(is) => is,
};
@ -126,6 +137,7 @@ mod test {
use super::*;
use std::ffi::OsString;
use options::flags;
use options::parser::Flag;
pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
@ -134,17 +146,17 @@ mod test {
}
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
#[test]
fn $name() {
use options::parser::{Args, Arg};
use std::ffi::OsString;
use options::parser::Arg;
use options::test::parse_for_test;
use options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB ];
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let results = Args(TEST_ARGS).parse(bits.iter());
assert_eq!($type::deduce(&results.unwrap().flags), $result);
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
assert_eq!(result, $result);
}
}
};
}
@ -153,21 +165,23 @@ mod test {
use super::*;
// Default behaviour
test!(empty: SortField <- [] => Ok(SortField::default()));
test!(empty: SortField <- []; Both => Ok(SortField::default()));
// Sort field arguments
test!(one_arg: SortField <- ["--sort=cr"] => Ok(SortField::CreatedDate));
test!(one_long: SortField <- ["--sort=size"] => Ok(SortField::Size));
test!(one_short: SortField <- ["-saccessed"] => Ok(SortField::AccessedDate));
test!(lowercase: SortField <- ["--sort", "name"] => Ok(SortField::Name(SortCase::Sensitive)));
test!(uppercase: SortField <- ["--sort", "Name"] => Ok(SortField::Name(SortCase::Insensitive)));
test!(one_arg: SortField <- ["--sort=cr"]; Both => Ok(SortField::CreatedDate));
test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size));
test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::Sensitive)));
test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::Insensitive)));
// Errors
test!(error: SortField <- ["--sort=colour"] => Err(Misfire::bad_argument(&flags::SORT, &os("colour"), super::SORTS)));
test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::bad_argument(&flags::SORT, &os("colour"), super::SORTS)));
// Overriding
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"] => Ok(SortField::ModifiedDate));
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"] => Ok(SortField::Extension(SortCase::Insensitive)));
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::Insensitive)));
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"))));
}
@ -175,16 +189,20 @@ mod test {
use super::*;
// Default behaviour
test!(empty: DotFilter <- [] => Ok(DotFilter::JustFiles));
test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles));
// --all
test!(all: DotFilter <- ["--all"] => Ok(DotFilter::Dotfiles));
test!(all_all: DotFilter <- ["--all", "-a"] => Ok(DotFilter::DotfilesAndDots));
test!(all_all_2: DotFilter <- ["-aa"] => Ok(DotFilter::DotfilesAndDots));
test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles));
test!(all_all: DotFilter <- ["--all", "-a"]; 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_4: DotFilter <- ["-aaa"]; Complain => Err(Misfire::Conflict(&flags::ALL, &flags::ALL)));
// --all and --tree
test!(tree_a: DotFilter <- ["-Ta"] => Ok(DotFilter::Dotfiles));
test!(tree_aa: DotFilter <- ["-Taa"] => Err(Misfire::TreeAllAll));
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));
}
@ -198,9 +216,15 @@ mod test {
}
// Various numbers of globs
test!(none: IgnorePatterns <- [] => Ok(IgnorePatterns::empty()));
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"] => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"] => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
test!(loads: IgnorePatterns <- ["-I*|?|.|*"] => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
// 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'))));
}
}

View File

@ -72,9 +72,13 @@ impl HelpString {
/// Determines how to show help, if at all, based on the users
/// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown.
///
/// 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<(), HelpString> {
if matches.has(&flags::HELP) {
let only_long = matches.has(&flags::LONG);
if matches.count(&flags::HELP) > 0 {
let only_long = matches.count(&flags::LONG) > 0;
let git = cfg!(feature="git");
let xattrs = xattr::ENABLED;
Err(HelpString { only_long, git, xattrs })
@ -126,21 +130,21 @@ mod test {
#[test]
fn help() {
let args = [ os("--help") ];
let opts = Options::getopts(&args);
let opts = Options::parse(&args, None);
assert!(opts.is_err())
}
#[test]
fn help_with_file() {
let args = [ os("--help"), os("me") ];
let opts = Options::getopts(&args);
let opts = Options::parse(&args, None);
assert!(opts.is_err())
}
#[test]
fn unhelpful() {
let args = [];
let opts = Options::getopts(&args);
let opts = Options::parse(&args, None);
assert!(opts.is_ok()) // no help when --help isnt passed
}
}

View File

@ -5,7 +5,7 @@ use std::num::ParseIntError;
use glob;
use options::{HelpString, VersionString};
use options::parser::{Arg, ParseError};
use options::parser::{Arg, Flag, ParseError};
/// A list of legal choices for an argument-taking option
@ -36,6 +36,9 @@ pub enum Misfire {
/// The user wanted the version number.
Version(VersionString),
/// An option was given twice or more in strict mode.
Duplicate(Flag, Flag),
/// Two options were given that conflict with one another.
Conflict(&'static Arg, &'static Arg),
@ -89,10 +92,11 @@ impl fmt::Display for Misfire {
match *self {
BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no value {:?} (Choices: {})", a, b, c),
InvalidOptions(ref e) => write!(f, "{:?}", e),
InvalidOptions(ref e) => write!(f, "{}", e),
Help(ref text) => write!(f, "{}", text),
Version(ref version) => write!(f, "{}", version),
Conflict(ref a, ref b) => write!(f, "Option {} conflicts with option {}.", a, b),
Duplicate(ref a, ref b) => write!(f, "Flag {:?} conflicts with flag {:?}.", a, b),
Useless(ref a, false, ref b) => write!(f, "Option {} is useless without option {}.", a, b),
Useless(ref a, true, ref b) => write!(f, "Option {} is useless given option {}.", a, b),
Useless2(ref a, ref b1, ref b2) => write!(f, "Option {} is useless without options {} or {}.", a, b1, b2),
@ -102,3 +106,16 @@ impl fmt::Display for Misfire {
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ParseError::*;
match *self {
NeedsValue { ref flag } => write!(f, "Flag {} needs a value", flag),
ForbiddenValue { ref flag } => write!(f, "Flag {} cannot take a value", flag),
UnknownShortArgument { ref attempt } => write!(f, "Unknown argument -{}", *attempt as char),
UnknownArgument { ref attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()),
}
}
}

View File

@ -112,13 +112,22 @@ pub struct Options {
impl Options {
/// Call getopts on the given slice of command-line strings.
/// Parse the given iterator of command-line strings into an Options
/// struct and a list of free filenames, using the environment variables
/// for extra options.
#[allow(unused_results)]
pub fn getopts<'args, I>(args: I) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString> {
use options::parser::Matches;
pub fn parse<'args, I, V>(args: I, vars: V) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString>,
V: Vars {
use options::parser::{Matches, Strictness};
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args) {
let strictness = match vars.get("EXA_STRICT") {
None => Strictness::UseLastArguments,
Some(ref t) if t.is_empty() => Strictness::UseLastArguments,
_ => Strictness::ComplainAboutRedundantArguments,
};
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) {
Ok(m) => m,
Err(e) => return Err(Misfire::InvalidOptions(e)),
};
@ -126,7 +135,7 @@ impl Options {
HelpString::deduce(&flags).map_err(Misfire::Help)?;
VersionString::deduce(&flags).map_err(Misfire::Version)?;
let options = Options::deduce(&flags)?;
let options = Options::deduce(&flags, vars)?;
Ok((options, frees))
}
@ -136,33 +145,84 @@ impl Options {
pub fn should_scan_for_git(&self) -> bool {
match self.view.mode {
Mode::Details(details::Options { table: Some(ref table), .. }) |
Mode::GridDetails(_, details::Options { table: Some(ref table), .. }) => table.should_scan_for_git(),
Mode::GridDetails(_, details::Options { table: Some(ref table), .. }) => table.extra_columns.should_scan_for_git(),
_ => false,
}
}
/// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed.
fn deduce(matches: &MatchedFlags) -> Result<Options, Misfire> {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<Options, Misfire> {
let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches)?;
let view = View::deduce(matches, vars)?;
Ok(Options { dir_action, view, filter })
}
}
/// Mockable wrapper for `std::env::var_os`.
pub trait Vars {
fn get(&self, name: &'static str) -> Option<OsString>;
}
#[cfg(test)]
mod test {
use super::{Options, Misfire, flags};
pub mod test {
use super::{Options, Misfire, Vars, flags};
use options::parser::{Arg, MatchedFlags};
use std::ffi::OsString;
use fs::filter::{SortField, SortCase};
// Test impl that just returns the value it has.
impl Vars for Option<OsString> {
fn get(&self, _name: &'static str) -> Option<OsString> {
self.clone()
}
}
#[derive(PartialEq, Debug)]
pub enum Strictnesses {
Last,
Complain,
Both,
}
/// This function gets used by the other testing modules.
/// It can run with one or both strictness values: if told to run with
/// both, then both should resolve to the same result.
///
/// It returns a vector with one or two elements in.
/// These elements can then be tested with assert_eq or what have you.
pub fn parse_for_test<T, F>(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec<T>
where F: Fn(&MatchedFlags) -> T
{
use self::Strictnesses::*;
use options::parser::{Args, Strictness};
use std::ffi::OsString;
let bits = inputs.into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let mut rezzies = Vec::new();
if strictnesses == Last || strictnesses == Both {
let results = Args(args).parse(bits.iter(), Strictness::UseLastArguments);
rezzies.push(get(&results.unwrap().flags));
}
if strictnesses == Complain || strictnesses == Both {
let results = Args(args).parse(bits.iter(), Strictness::ComplainAboutRedundantArguments);
rezzies.push(get(&results.unwrap().flags));
}
rezzies
}
/// Creates an `OSStr` (used in tests)
#[cfg(test)]
fn os(input: &'static str) -> OsString {
fn os(input: &str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
@ -171,106 +231,36 @@ mod test {
#[test]
fn files() {
let args = [ os("this file"), os("that file") ];
let outs = Options::getopts(&args).unwrap().1;
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::getopts(&nothing).unwrap().1;
let outs = Options::parse(&nothing, None).unwrap().1;
assert!(outs.is_empty()); // Listing the `.` directory is done in main.rs
}
#[test]
fn just_binary() {
let args = [ os("--binary") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::BINARY, false, &flags::LONG))
}
#[test]
fn just_bytes() {
let args = [ os("--bytes") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::BYTES, false, &flags::LONG))
}
#[test]
fn long_across() {
let args = [ os("--long"), os("--across") ];
let opts = Options::getopts(&args);
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::getopts(&args);
let opts = Options::parse(&args, None);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
}
#[test]
fn just_header() {
let args = [ os("--header") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::HEADER, false, &flags::LONG))
}
#[test]
fn just_group() {
let args = [ os("--group") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::GROUP, false, &flags::LONG))
}
#[test]
fn just_inode() {
let args = [ os("--inode") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::INODE, false, &flags::LONG))
}
#[test]
fn just_links() {
let args = [ os("--links") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::LINKS, false, &flags::LONG))
}
#[test]
fn just_blocks() {
let args = [ os("--blocks") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::BLOCKS, false, &flags::LONG))
}
#[test]
fn test_sort_size() {
let args = [ os("--sort=size") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
}
#[test]
fn test_sort_name() {
let args = [ os("--sort=name") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
}
#[test]
fn test_sort_name_lowercase() {
let args = [ os("--sort=Name") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
}
#[test]
#[cfg(feature="git")]
fn just_git() {
let args = [ os("--git") ];
let opts = Options::getopts(&args);
let opts = Options::parse(&args, None);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::GIT, false, &flags::LONG))
}
}

View File

@ -31,6 +31,8 @@
use std::ffi::{OsStr, OsString};
use std::fmt;
use options::Misfire;
/// A **short argument** is a single ASCII character.
pub type ShortArg = u8;
@ -50,7 +52,7 @@ pub enum Flag {
}
impl Flag {
fn matches(&self, arg: &Arg) -> bool {
pub fn matches(&self, arg: &Arg) -> bool {
match *self {
Flag::Short(short) => arg.short == Some(short),
Flag::Long(long) => arg.long == long,
@ -58,10 +60,17 @@ impl Flag {
}
}
impl fmt::Display for Flag {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Flag::Short(short) => write!(f, "-{}", short as char),
Flag::Long(long) => write!(f, "--{}", long),
}
}
}
/// Whether redundant arguments should be considered a problem.
#[derive(PartialEq, Debug)]
#[allow(dead_code)] // until strict mode is actually implemented
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Strictness {
/// Throw an error when an argument doesnt do anything, either because
@ -122,7 +131,7 @@ impl Args {
/// Iterates over the given list of command-line arguments and parses
/// them into a list of matched flags and free strings.
pub fn parse<'args, I>(&self, inputs: I) -> Result<Matches<'args>, ParseError>
pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
where I: IntoIterator<Item=&'args OsString> {
use std::os::unix::ffi::OsStrExt;
use self::TakesValue::*;
@ -267,17 +276,17 @@ impl Args {
}
}
Ok(Matches { frees, flags: MatchedFlags { flags: result_flags } })
Ok(Matches { frees, flags: MatchedFlags { flags: result_flags, strictness } })
}
fn lookup_short<'a>(&self, short: ShortArg) -> Result<&Arg, ParseError> {
fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> {
match self.0.into_iter().find(|arg| arg.short == Some(short)) {
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownShortArgument { attempt: short })
}
}
fn lookup_long<'a>(&self, long: &'a OsStr) -> Result<&Arg, ParseError> {
fn lookup_long<'b>(&self, long: &'b OsStr) -> Result<&Arg, ParseError> {
match self.0.into_iter().find(|arg| arg.long == long) {
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() })
@ -308,34 +317,93 @@ pub struct MatchedFlags<'args> {
/// we usually want the one nearest the end to count, and to know this,
/// we need to know where they are in relation to one another.
flags: Vec<(Flag, Option<&'args OsStr>)>,
/// Whether to check for duplicate or redundant arguments.
strictness: Strictness,
}
impl<'a> MatchedFlags<'a> {
/// Whether the given argument was specified.
pub fn has(&self, arg: &Arg) -> bool {
self.flags.iter().rev()
.find(|tuple| tuple.1.is_none() && tuple.0.matches(arg))
.is_some()
/// 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> {
self.has_where(|flag| flag.matches(arg)).map(|flag| flag.is_some())
}
/// If the given argument was specified, return its value.
/// The value is not guaranteed to be valid UTF-8.
pub fn get(&self, arg: &Arg) -> Option<&OsStr> {
self.flags.iter().rev()
.find(|tuple| tuple.1.is_some() && tuple.0.matches(arg))
.map(|tuple| tuple.1.unwrap())
/// Returns the first found argument that satisfies the predicate, or
/// nothing if none is found, or an error in strict mode if multiple
/// 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>
where P: Fn(&Flag) -> bool {
if self.is_strict() {
let all = self.flags.iter()
.filter(|tuple| tuple.1.is_none() && predicate(&tuple.0))
.collect::<Vec<_>>();
if all.len() < 2 { Ok(all.first().map(|t| &t.0)) }
else { Err(Misfire::Duplicate(all[0].0.clone(), all[1].0.clone())) }
}
else {
let any = self.flags.iter().rev()
.find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
.map(|tuple| &tuple.0);
Ok(any)
}
}
// This code could probably be better.
// Both has and get immediately begin with a conditional, which makes
// me think the functionality could be moved to inside Strictness.
/// 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> {
self.get_where(|flag| flag.matches(arg))
}
/// Returns the value of the argument that matches the predicate if it
/// was specified, nothing if it wasn't, and an error in strict mode if
/// 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>
where P: Fn(&Flag) -> bool {
if self.is_strict() {
let those = self.flags.iter()
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
.collect::<Vec<_>>();
if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) }
else { Err(Misfire::Duplicate(those[0].0.clone(), those[1].0.clone())) }
}
else {
let found = self.flags.iter().rev()
.find(|tuple| tuple.1.is_some() && predicate(&tuple.0))
.map(|tuple| tuple.1.unwrap());
Ok(found)
}
}
// Its annoying that has and get wont work when accidentally given
// flags that do/dont take values, but this should be caught by tests.
/// Counts the number of occurrences of the given argument.
/// Counts the number of occurrences of the given argument, even in
/// strict mode.
pub fn count(&self, arg: &Arg) -> usize {
self.flags.iter()
.filter(|tuple| tuple.0.matches(arg))
.count()
}
/// Checks whether strict mode is on. This is usually done from within
/// has and get, but its available in an emergency.
pub fn is_strict(&self) -> bool {
self.strictness == Strictness::ComplainAboutRedundantArguments
}
}
@ -433,6 +501,13 @@ mod split_test {
mod parse_test {
use super::*;
pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
macro_rules! test {
($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => {
#[test]
@ -445,15 +520,11 @@ mod parse_test {
let frees: Vec<OsString> = $frees.as_ref().into_iter().map(|&o| os(o)).collect();
let frees: Vec<&OsStr> = frees.iter().map(|os| os.as_os_str()).collect();
// And again for the flags
let flags: Vec<(Flag, Option<&OsStr>)> = $flags
.as_ref()
.into_iter()
.map(|&(ref f, ref os): &(Flag, Option<&'static str>)| (f.clone(), os.map(OsStr::new)))
.collect();
let flags = <[_]>::into_vec(Box::new($flags));
let got = Args(TEST_ARGS).parse(inputs.iter());
let expected = Ok(Matches { frees, flags: MatchedFlags { flags } });
let strictness = Strictness::UseLastArguments; // this isnt even used
let got = Args(TEST_ARGS).parse(inputs.iter(), strictness);
let expected = Ok(Matches { frees, flags: MatchedFlags { flags, strictness } });
assert_eq!(got, expected);
}
};
@ -463,8 +534,9 @@ mod parse_test {
fn $name() {
use self::ParseError::*;
let strictness = Strictness::UseLastArguments; // this isnt even used
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let got = Args(TEST_ARGS).parse(bits.iter());
let got = Args(TEST_ARGS).parse(bits.iter(), strictness);
assert_eq!(got, Err($error));
}
@ -498,8 +570,8 @@ mod parse_test {
// Long args with values
test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") });
test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count") });
test!(arg_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some("4")) ]);
test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some("4")) ]);
test!(arg_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]);
test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]);
// Short args
@ -511,11 +583,11 @@ mod parse_test {
// Short args with values
test!(bad_short: ["-l=equals"] => error ForbiddenValue { flag: Flag::Short(b'l') });
test!(short_none: ["-c"] => error NeedsValue { flag: Flag::Short(b'c') });
test!(short_arg_eq: ["-c=4"] => frees: [], flags: [(Flag::Short(b'c'), Some("4")) ]);
test!(short_arg_then: ["-c", "4"] => frees: [], flags: [(Flag::Short(b'c'), Some("4")) ]);
test!(short_two_together: ["-lctwo"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some("two")) ]);
test!(short_two_equals: ["-lc=two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some("two")) ]);
test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some("two")) ]);
test!(short_arg_eq: ["-c=4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]);
test!(short_arg_then: ["-c", "4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]);
test!(short_two_together: ["-lctwo"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
test!(short_two_equals: ["-lc=two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
// Unknown args
@ -536,8 +608,12 @@ mod matches_test {
($name:ident: $input:expr, has $param:expr => $result:expr) => {
#[test]
fn $name() {
let flags = MatchedFlags { flags: $input.to_vec() };
assert_eq!(flags.has(&$param), $result);
let flags = MatchedFlags {
flags: $input.to_vec(),
strictness: Strictness::UseLastArguments,
};
assert_eq!(flags.has(&$param), Ok($result));
}
};
}
@ -557,8 +633,13 @@ mod matches_test {
#[test]
fn only_count() {
let everything = os("everything");
let flags = MatchedFlags { flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ] };
assert_eq!(flags.get(&COUNT), Some(&*everything));
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ],
strictness: Strictness::UseLastArguments,
};
assert_eq!(flags.get(&COUNT), Ok(Some(&*everything)));
}
#[test]
@ -568,16 +649,17 @@ mod matches_test {
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)),
(Flag::Short(b'c'), Some(&*nothing)) ]
(Flag::Short(b'c'), Some(&*nothing)) ],
strictness: Strictness::UseLastArguments,
};
assert_eq!(flags.get(&COUNT), Some(&*nothing));
assert_eq!(flags.get(&COUNT), Ok(Some(&*nothing)));
}
#[test]
fn no_count() {
let flags = MatchedFlags { flags: Vec::new() };
let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments };
assert!(!flags.has(&COUNT));
assert!(!flags.has(&COUNT).unwrap());
}
}

View File

@ -17,8 +17,10 @@ impl VersionString {
/// Determines how to show the version, if at all, based on the users
/// 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<(), VersionString> {
if matches.has(&flags::VERSION) {
if matches.count(&flags::VERSION) > 0 {
Err(VersionString { cargo: env!("CARGO_PKG_VERSION") })
}
else {
@ -52,7 +54,7 @@ mod test {
#[test]
fn help() {
let args = [ os("--version") ];
let opts = Options::getopts(&args);
let opts = Options::parse(&args, None);
assert!(opts.is_err())
}
}

View File

@ -1,25 +1,22 @@
use std::env::var_os;
use output::Colours;
use output::{View, Mode, grid, details};
use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions};
use output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions};
use output::file_name::{Classify, FileStyle};
use output::time::TimeFormat;
use options::{flags, Misfire};
use options::{flags, Misfire, Vars};
use options::parser::MatchedFlags;
use fs::feature::xattr;
use info::filetype::FileExtensions;
impl View {
/// Determine which view to use and all of that views arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<View, Misfire> {
let mode = Mode::deduce(matches)?;
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<View, Misfire> {
let mode = Mode::deduce(matches, vars)?;
let colours = Colours::deduce(matches)?;
let style = FileStyle::deduce(matches);
let style = FileStyle::deduce(matches)?;
Ok(View { mode, colours, style })
}
}
@ -28,21 +25,21 @@ impl View {
impl Mode {
/// Determine the mode from the command-line arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<Mode, Misfire> {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<Mode, Misfire> {
use options::misfire::Misfire::*;
let long = || {
if matches.has(&flags::ACROSS) && !matches.has(&flags::GRID) {
if matches.has(&flags::ACROSS)? && !matches.has(&flags::GRID)? {
Err(Useless(&flags::ACROSS, true, &flags::LONG))
}
else if matches.has(&flags::ONE_LINE) {
else if matches.has(&flags::ONE_LINE)? {
Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
}
else {
Ok(details::Options {
table: Some(TableOptions::deduce(matches)?),
header: matches.has(&flags::HEADER),
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED),
header: matches.has(&flags::HEADER)?,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
})
}
};
@ -50,15 +47,15 @@ impl Mode {
let long_options_scan = || {
for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
if matches.has(option) {
if matches.is_strict() && matches.has(option)? {
return Err(Useless(*option, false, &flags::LONG));
}
}
if cfg!(feature="git") && matches.has(&flags::GIT) {
if cfg!(feature="git") && matches.has(&flags::GIT)? {
Err(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)? {
Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))
}
else {
@ -67,27 +64,27 @@ impl Mode {
};
let other_options_scan = || {
if let Some(width) = TerminalWidth::deduce()?.width() {
if matches.has(&flags::ONE_LINE) {
if matches.has(&flags::ACROSS) {
if let Some(width) = TerminalWidth::deduce(vars)?.width() {
if matches.has(&flags::ONE_LINE)? {
if matches.has(&flags::ACROSS)? {
Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE))
}
else {
Ok(Mode::Lines)
}
}
else if matches.has(&flags::TREE) {
else if matches.has(&flags::TREE)? {
let details = details::Options {
table: None,
header: false,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED),
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
};
Ok(Mode::Details(details))
}
else {
let grid = grid::Options {
across: matches.has(&flags::ACROSS),
across: matches.has(&flags::ACROSS)?,
console_width: width,
};
@ -99,11 +96,11 @@ impl Mode {
// as the programs stdout being connected to a file, then
// fallback to the lines view.
if matches.has(&flags::TREE) {
if matches.has(&flags::TREE)? {
let details = details::Options {
table: None,
header: false,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED),
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
};
Ok(Mode::Details(details))
@ -114,9 +111,9 @@ impl Mode {
}
};
if matches.has(&flags::LONG) {
if matches.has(&flags::LONG)? {
let details = long()?;
if matches.has(&flags::GRID) {
if matches.has(&flags::GRID)? {
match other_options_scan()? {
Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)),
others => return Ok(others),
@ -153,8 +150,8 @@ 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() -> Result<TerminalWidth, Misfire> {
if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
fn deduce<V: Vars>(vars: V) -> Result<TerminalWidth, Misfire> {
if let Some(columns) = vars.get("COLUMNS").and_then(|s| s.into_string().ok()) {
match columns.parse() {
Ok(width) => Ok(TerminalWidth::Set(width)),
Err(e) => Err(Misfire::FailedParse(e)),
@ -180,17 +177,26 @@ impl TerminalWidth {
impl TableOptions {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
Ok(TableOptions {
env: Environment::load_all(),
time_format: TimeFormat::deduce(matches)?,
size_format: SizeFormat::deduce(matches)?,
time_types: TimeTypes::deduce(matches)?,
inode: matches.has(&flags::INODE),
links: matches.has(&flags::LINKS),
blocks: matches.has(&flags::BLOCKS),
group: matches.has(&flags::GROUP),
git: cfg!(feature="git") && matches.has(&flags::GIT),
})
let env = Environment::load_all();
let time_format = TimeFormat::deduce(matches)?;
let size_format = SizeFormat::deduce(matches)?;
let extra_columns = Columns::deduce(matches)?;
Ok(TableOptions { env, time_format, size_format, extra_columns })
}
}
impl Columns {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
let time_types = TimeTypes::deduce(matches)?;
let git = cfg!(feature="git") && matches.has(&flags::GIT)?;
let blocks = matches.has(&flags::BLOCKS)?;
let group = matches.has(&flags::GROUP)?;
let inode = matches.has(&flags::INODE)?;
let links = matches.has(&flags::LINKS)?;
Ok(Columns { time_types, git, blocks, group, inode, links })
}
}
@ -206,27 +212,26 @@ impl SizeFormat {
/// involves the `--binary` or `--bytes` flags, and these conflict with
/// each other.
fn deduce(matches: &MatchedFlags) -> Result<SizeFormat, Misfire> {
let binary = matches.has(&flags::BINARY);
let bytes = matches.has(&flags::BYTES);
let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?;
match (binary, bytes) {
(true, true ) => Err(Misfire::Conflict(&flags::BINARY, &flags::BYTES)),
(true, false) => Ok(SizeFormat::BinaryBytes),
(false, true ) => Ok(SizeFormat::JustBytes),
(false, false) => Ok(SizeFormat::DecimalBytes),
}
Ok(match flag {
Some(f) if f.matches(&flags::BINARY) => SizeFormat::BinaryBytes,
Some(f) if f.matches(&flags::BYTES) => SizeFormat::JustBytes,
_ => SizeFormat::DecimalBytes,
})
}
}
const TIME_STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
impl TimeFormat {
/// Determine how time should be formatted in timestamp columns.
fn deduce(matches: &MatchedFlags) -> Result<TimeFormat, Misfire> {
pub use output::time::{DefaultFormat, ISOFormat};
const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
let word = match matches.get(&flags::TIME_STYLE) {
let word = match matches.get(&flags::TIME_STYLE)? {
Some(w) => w,
None => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
};
@ -244,7 +249,7 @@ impl TimeFormat {
Ok(TimeFormat::FullISO)
}
else {
Err(Misfire::bad_argument(&flags::TIME_STYLE, word, STYLES))
Err(Misfire::bad_argument(&flags::TIME_STYLE, word, TIME_STYLES))
}
}
}
@ -265,10 +270,10 @@ impl TimeTypes {
/// option, but passing *no* options means that the user just wants to
/// see the default set.
fn deduce(matches: &MatchedFlags) -> Result<TimeTypes, Misfire> {
let possible_word = matches.get(&flags::TIME);
let modified = matches.has(&flags::MODIFIED);
let created = matches.has(&flags::CREATED);
let accessed = matches.has(&flags::ACCESSED);
let possible_word = matches.get(&flags::TIME)?;
let modified = matches.has(&flags::MODIFIED)?;
let created = matches.has(&flags::CREATED)?;
let accessed = matches.has(&flags::ACCESSED)?;
if let Some(word) = possible_word {
if modified {
@ -329,13 +334,14 @@ impl Default for TerminalColours {
}
}
const COLOURS: &[&str] = &["always", "auto", "never"];
impl TerminalColours {
/// Determine which terminal colour conditions to use.
fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
const COLOURS: &[&str] = &["always", "auto", "never"];
let word = match matches.get(&flags::COLOR).or_else(|| matches.get(&flags::COLOUR)) {
let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
Some(w) => w,
None => return Ok(TerminalColours::default()),
};
@ -362,7 +368,7 @@ impl Colours {
let tc = TerminalColours::deduce(matches)?;
if tc == Always || (tc == Automatic && TERM_WIDTH.is_some()) {
let scale = matches.has(&flags::COLOR_SCALE) || matches.has(&flags::COLOUR_SCALE);
let scale = matches.has(&flags::COLOR_SCALE)? || matches.has(&flags::COLOUR_SCALE)?;
Ok(Colours::colourful(scale))
}
else {
@ -374,17 +380,19 @@ impl Colours {
impl FileStyle {
fn deduce(matches: &MatchedFlags) -> FileStyle {
let classify = Classify::deduce(matches);
fn deduce(matches: &MatchedFlags) -> Result<FileStyle, Misfire> {
let classify = Classify::deduce(matches)?;
let exts = FileExtensions;
FileStyle { classify, exts }
Ok(FileStyle { classify, exts })
}
}
impl Classify {
fn deduce(matches: &MatchedFlags) -> Classify {
if matches.has(&flags::CLASSIFY) { Classify::AddFileIndicators }
else { Classify::JustFilenames }
fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
let flagged = matches.has(&flags::CLASSIFY)?;
Ok(if flagged { Classify::AddFileIndicators }
else { Classify::JustFilenames })
}
}
@ -409,6 +417,10 @@ mod test {
use super::*;
use std::ffi::OsString;
use options::flags;
use options::parser::{Flag, Arg};
use options::test::parse_for_test;
use options::test::Strictnesses::*;
pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
@ -416,19 +428,74 @@ mod test {
os
}
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE,
&flags::TIME, &flags::MODIFIED, &flags::CREATED, &flags::ACCESSED,
&flags::COLOR, &flags::COLOUR,
&flags::HEADER, &flags::GROUP, &flags::INODE,
&flags::LINKS, &flags::BLOCKS, &flags::LONG,
&flags::GRID, &flags::ACROSS, &flags::ONE_LINE ];
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
/// Macro that writes a test.
/// If testing both strictnesses, theyll both be done in the same function.
#[test]
fn $name() {
use options::parser::{Args, Arg};
use std::ffi::OsString;
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
assert_eq!(result, $result);
}
}
};
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES,
&flags::TIME, &flags::MODIFIED, &flags::CREATED, &flags::ACCESSED ];
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => {
/// Special macro for testing Err results.
/// This is needed because sometimes the Ok type doesnt implement PartialEq.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
assert_eq!(result.unwrap_err(), $result);
}
}
};
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let results = Args(TEST_ARGS).parse(bits.iter());
assert_eq!($type::deduce(&results.unwrap().flags), $result);
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => like $pat:pat) => {
/// More general macro for testing against a pattern.
/// Instead of using PartialEq, this just tests if it matches a pat.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
_ => assert!(false),
}
}
}
};
($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => err $result:expr) => {
/// Like above, but with $vars.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, $vars)) {
assert_eq!(result.unwrap_err(), $result);
}
}
};
($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => like $pat:pat) => {
/// Like further above, but with $vars.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, $vars)) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
_ => assert!(false),
}
}
}
};
}
@ -437,10 +504,51 @@ mod test {
mod size_formats {
use super::*;
test!(empty: SizeFormat <- [] => Ok(SizeFormat::DecimalBytes));
test!(binary: SizeFormat <- ["--binary"] => Ok(SizeFormat::BinaryBytes));
test!(bytes: SizeFormat <- ["--bytes"] => Ok(SizeFormat::JustBytes));
test!(both: SizeFormat <- ["--binary", "--bytes"] => Err(Misfire::Conflict(&flags::BINARY, &flags::BYTES)));
// Default behaviour
test!(empty: SizeFormat <- []; Both => Ok(SizeFormat::DecimalBytes));
// Individual flags
test!(binary: SizeFormat <- ["--binary"]; Both => Ok(SizeFormat::BinaryBytes));
test!(bytes: SizeFormat <- ["--bytes"]; Both => Ok(SizeFormat::JustBytes));
// Overriding
test!(both_1: SizeFormat <- ["--binary", "--binary"]; Last => Ok(SizeFormat::BinaryBytes));
test!(both_2: SizeFormat <- ["--bytes", "--binary"]; Last => Ok(SizeFormat::BinaryBytes));
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")));
}
mod time_formats {
use super::*;
use std::ffi::OsStr;
// These tests use pattern matching because TimeFormat doesnt
// implement PartialEq.
// Default behaviour
test!(empty: TimeFormat <- []; Both => like Ok(TimeFormat::DefaultFormat(_)));
// Individual settings
test!(default: TimeFormat <- ["--time-style=default"]; Both => like Ok(TimeFormat::DefaultFormat(_)));
test!(iso: TimeFormat <- ["--time-style", "iso"]; Both => like Ok(TimeFormat::ISOFormat(_)));
test!(long_iso: TimeFormat <- ["--time-style=long-iso"]; Both => like Ok(TimeFormat::LongISO));
test!(full_iso: TimeFormat <- ["--time-style", "full-iso"]; Both => like Ok(TimeFormat::FullISO));
// Overriding
test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"]; Last => like Ok(TimeFormat::ISOFormat(_)));
test!(actual_2: TimeFormat <- ["--time-style=default", "--time-style", "iso"]; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style")));
test!(nevermind: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"]; Last => like Ok(TimeFormat::FullISO));
test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"]; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style")));
// Errors
test!(daily: TimeFormat <- ["--time-style=24-hour"]; Both => err Misfire::bad_argument(&flags::TIME_STYLE, OsStr::new("24-hour"), TIME_STYLES));
}
@ -448,30 +556,113 @@ mod test {
use super::*;
// Default behaviour
test!(empty: TimeTypes <- [] => Ok(TimeTypes::default()));
test!(modified: TimeTypes <- ["--modified"] => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(m: TimeTypes <- ["-m"] => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_mod: TimeTypes <- ["--time=modified"] => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_m: TimeTypes <- ["-tmod"] => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(empty: TimeTypes <- []; Both => Ok(TimeTypes::default()));
test!(acc: TimeTypes <- ["--accessed"] => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(a: TimeTypes <- ["-u"] => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(time_acc: TimeTypes <- ["--time", "accessed"] => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(time_a: TimeTypes <- ["-t", "acc"] => Ok(TimeTypes { accessed: true, modified: false, created: false }));
// Modified
test!(modified: TimeTypes <- ["--modified"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(m: TimeTypes <- ["-m"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_mod: TimeTypes <- ["--time=modified"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_m: TimeTypes <- ["-tmod"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(cr: TimeTypes <- ["--created"] => Ok(TimeTypes { accessed: false, modified: false, created: true }));
test!(c: TimeTypes <- ["-U"] => Ok(TimeTypes { accessed: false, modified: false, created: true }));
test!(time_cr: TimeTypes <- ["--time=created"] => Ok(TimeTypes { accessed: false, modified: false, created: true }));
test!(time_c: TimeTypes <- ["-tcr"] => Ok(TimeTypes { accessed: false, modified: false, created: true }));
// Accessed
test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(a: TimeTypes <- ["-u"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(time_acc: TimeTypes <- ["--time", "accessed"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(time_a: TimeTypes <- ["-t", "acc"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false }));
// Created
test!(cr: TimeTypes <- ["--created"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true }));
test!(c: TimeTypes <- ["-U"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true }));
test!(time_cr: TimeTypes <- ["--time=created"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true }));
test!(time_c: TimeTypes <- ["-tcr"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true }));
// Multiples
test!(time_uu: TimeTypes <- ["-uU"] => Ok(TimeTypes { accessed: true, modified: false, created: true }));
// Overriding
test!(time_mc: TimeTypes <- ["-tcr", "-tmod"] => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_uu: TimeTypes <- ["-uU"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: true }));
// Errors
test!(time_tea: TimeTypes <- ["--time=tea"] => Err(Misfire::bad_argument(&flags::TIME, &os("tea"), super::TIMES)));
test!(time_ea: TimeTypes <- ["-tea"] => Err(Misfire::bad_argument(&flags::TIME, &os("ea"), super::TIMES)));
test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::bad_argument(&flags::TIME, &os("tea"), super::TIMES));
test!(time_ea: TimeTypes <- ["-tea"]; Both => err Misfire::bad_argument(&flags::TIME, &os("ea"), super::TIMES));
// Overriding
test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err Misfire::Duplicate(Flag::Short(b't'), Flag::Short(b't')));
}
mod colourses {
use super::*;
// Default
test!(empty: TerminalColours <- []; Both => Ok(TerminalColours::default()));
// --colour
test!(u_always: TerminalColours <- ["--colour=always"]; Both => Ok(TerminalColours::Always));
test!(u_auto: TerminalColours <- ["--colour", "auto"]; Both => Ok(TerminalColours::Automatic));
test!(u_never: TerminalColours <- ["--colour=never"]; Both => Ok(TerminalColours::Never));
// --color
test!(no_u_always: TerminalColours <- ["--color", "always"]; Both => Ok(TerminalColours::Always));
test!(no_u_auto: TerminalColours <- ["--color=auto"]; Both => Ok(TerminalColours::Automatic));
test!(no_u_never: TerminalColours <- ["--color", "never"]; Both => Ok(TerminalColours::Never));
// Errors
test!(no_u_error: TerminalColours <- ["--color=upstream"]; Both => err Misfire::bad_argument(&flags::COLOR, &os("upstream"), super::COLOURS)); // the error is for --color
test!(u_error: TerminalColours <- ["--colour=lovers"]; Both => err Misfire::bad_argument(&flags::COLOR, &os("lovers"), super::COLOURS)); // and so is this one!
// Overriding
test!(overridden_1: TerminalColours <- ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_2: TerminalColours <- ["--color=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_3: TerminalColours <- ["--colour=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_4: TerminalColours <- ["--color=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
test!(overridden_5: TerminalColours <- ["--colour=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
test!(overridden_6: TerminalColours <- ["--color=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("colour")));
test!(overridden_7: TerminalColours <- ["--colour=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("color")));
test!(overridden_8: TerminalColours <- ["--color=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("color")));
}
mod views {
use super::*;
use output::grid::Options as GridOptions;
// Default
test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_)));
// Grid views
test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _ })));
test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _ })));
test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _ })));
test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _ })));
// Lines views
test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines));
test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines));
// Details views
test!(long: Mode <- ["--long"], None; Both => like Ok(Mode::Details(_)));
test!(ell: Mode <- ["-l"], None; Both => like Ok(Mode::Details(_)));
// Grid-details views
test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_, _)));
test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_, _)));
// Options that do nothing without --long
test!(just_header: Mode <- ["--header"], None; Last => like Ok(Mode::Grid(_)));
test!(just_group: Mode <- ["--group"], None; Last => like Ok(Mode::Grid(_)));
test!(just_inode: Mode <- ["--inode"], None; Last => like Ok(Mode::Grid(_)));
test!(just_links: Mode <- ["--links"], None; Last => like Ok(Mode::Grid(_)));
test!(just_blocks: Mode <- ["--blocks"], None; Last => like Ok(Mode::Grid(_)));
test!(just_binary: Mode <- ["--binary"], None; Last => like Ok(Mode::Grid(_)));
test!(just_bytes: Mode <- ["--bytes"], 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));
}
}

View File

@ -23,7 +23,17 @@ pub struct Options {
pub env: Environment,
pub size_format: SizeFormat,
pub time_format: TimeFormat,
pub extra_columns: Columns,
}
/// Extra columns to display in the table.
#[derive(PartialEq, Debug)]
pub struct Columns {
/// At least one of these timestamps will be shown.
pub time_types: TimeTypes,
// The rest are just on/off
pub inode: bool,
pub links: bool,
pub blocks: bool,
@ -39,7 +49,7 @@ impl fmt::Debug for Options {
}
}
impl Options {
impl Columns {
pub fn should_scan_for_git(&self) -> bool {
self.git
}
@ -57,7 +67,7 @@ impl Options {
columns.push(Column::HardLinks);
}
columns.push(Column::FileSize(self.size_format));
columns.push(Column::FileSize);
if self.blocks {
columns.push(Column::Blocks);
@ -98,7 +108,7 @@ impl Options {
#[derive(Debug)]
pub enum Column {
Permissions,
FileSize(SizeFormat),
FileSize,
Timestamp(TimeType),
Blocks,
User,
@ -120,7 +130,7 @@ impl Column {
/// Get the alignment this column should use.
pub fn alignment(&self) -> Alignment {
match *self {
Column::FileSize(_)
Column::FileSize
| Column::HardLinks
| Column::Inode
| Column::Blocks
@ -134,7 +144,7 @@ impl Column {
pub fn header(&self) -> &'static str {
match *self {
Column::Permissions => "Permissions",
Column::FileSize(_) => "Size",
Column::FileSize => "Size",
Column::Timestamp(t) => t.header(),
Column::Blocks => "Blocks",
Column::User => "User",
@ -276,6 +286,7 @@ pub struct Table<'a> {
env: &'a Environment,
widths: TableWidths,
time_format: &'a TimeFormat,
size_format: SizeFormat,
}
#[derive(Clone)]
@ -285,9 +296,14 @@ pub struct Row {
impl<'a, 'f> Table<'a> {
pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> Table<'a> {
let colz = options.for_dir(dir);
let colz = options.extra_columns.for_dir(dir);
let widths = TableWidths::zero(colz.len());
Table { columns: colz, colours, env: &options.env, widths, time_format: &options.time_format }
Table { colours, widths,
columns: colz,
env: &options.env,
time_format: &options.time_format,
size_format: options.size_format,
}
}
pub fn widths(&self) -> &TableWidths {
@ -327,7 +343,7 @@ impl<'a, 'f> Table<'a> {
match *column {
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
Column::FileSize(fmt) => file.size().render(&self.colours, fmt, &self.env.numeric),
Column::FileSize => file.size().render(&self.colours, self.size_format, &self.env.numeric),
Column::HardLinks => file.links().render(&self.colours, &self.env.numeric),
Column::Inode => file.inode().render(&self.colours),
Column::Blocks => file.blocks().render(&self.colours),

View File

@ -1,3 +1,5 @@
//! Timestamp formatting.
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
use datetime::fmt::DateFormat;
use locale;
@ -6,13 +8,48 @@ use std::cmp;
use fs::fields::Time;
/// Every timestamp in exa needs to be rendered by a **time format**.
/// Formatting times is tricky, because how a timestamp is rendered can
/// depend on one or more of the following:
///
/// - The users locale, for printing the month name as “Feb”, or as “fév”,
/// or as “2月”;
/// - The current year, because certain formats will be less precise when
/// dealing with dates far in the past;
/// - The formatting style that the user asked for on the command-line.
///
/// Because not all formatting styles need the same data, they all have their
/// own enum variants. Its not worth looking the locale up if the formatter
/// prints month names as numbers.
///
/// 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)]
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),
/// 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),
/// Use the **long ISO format**, which specifies the timestamp down to the
/// minute using only numbers, without needing the locale or year.
LongISO,
/// Use the **full ISO format**, which specifies the timestamp down to the
/// millisecond and includes its offset down to the minute. This too uses
/// only numbers so doesnt require any special consideration.
FullISO,
}
// There are two different formatting functions because local and zoned
// timestamps are separate types.
impl TimeFormat {
pub fn format_local(&self, time: Time) -> String {
match *self {

1
xtests/error_long Normal file
View File

@ -0,0 +1 @@
Unknown argument --ternary

1
xtests/error_overvalued Normal file
View File

@ -0,0 +1 @@
Flag --long cannot take a value

1
xtests/error_short Normal file
View File

@ -0,0 +1 @@
Unknown argument -4

1
xtests/error_useless Normal file
View File

@ -0,0 +1 @@
Option --binary (-b) is useless without option --long (-l).

1
xtests/error_value Normal file
View File

@ -0,0 +1 @@
Flag --time needs a value

39
xtests/files_l_binary Normal file
View File

@ -0,0 +1,39 @@
.rw-r--r-- 1.0Ki cassowary  1 Jan 12:34 1_KiB
.rw-r--r-- 1.0Mi cassowary  1 Jan 12:34 1_MiB
.rw-r--r-- 1 cassowary  1 Jan 12:34 1_bytes
.rw-r--r-- 2.0Ki cassowary  1 Jan 12:34 2_KiB
.rw-r--r-- 2.0Mi cassowary  1 Jan 12:34 2_MiB
.rw-r--r-- 2 cassowary  1 Jan 12:34 2_bytes
.rw-r--r-- 3.0Ki cassowary  1 Jan 12:34 3_KiB
.rw-r--r-- 3.0Mi cassowary  1 Jan 12:34 3_MiB
.rw-r--r-- 3 cassowary  1 Jan 12:34 3_bytes
.rw-r--r-- 4.0Ki cassowary  1 Jan 12:34 4_KiB
.rw-r--r-- 4.0Mi cassowary  1 Jan 12:34 4_MiB
.rw-r--r-- 4 cassowary  1 Jan 12:34 4_bytes
.rw-r--r-- 5.0Ki cassowary  1 Jan 12:34 5_KiB
.rw-r--r-- 5.0Mi cassowary  1 Jan 12:34 5_MiB
.rw-r--r-- 5 cassowary  1 Jan 12:34 5_bytes
.rw-r--r-- 6.0Ki cassowary  1 Jan 12:34 6_KiB
.rw-r--r-- 6.0Mi cassowary  1 Jan 12:34 6_MiB
.rw-r--r-- 6 cassowary  1 Jan 12:34 6_bytes
.rw-r--r-- 7.0Ki cassowary  1 Jan 12:34 7_KiB
.rw-r--r-- 7.0Mi cassowary  1 Jan 12:34 7_MiB
.rw-r--r-- 7 cassowary  1 Jan 12:34 7_bytes
.rw-r--r-- 8.0Ki cassowary  1 Jan 12:34 8_KiB
.rw-r--r-- 8.0Mi cassowary  1 Jan 12:34 8_MiB
.rw-r--r-- 8 cassowary  1 Jan 12:34 8_bytes
.rw-r--r-- 9.0Ki cassowary  1 Jan 12:34 9_KiB
.rw-r--r-- 9.0Mi cassowary  1 Jan 12:34 9_MiB
.rw-r--r-- 9 cassowary  1 Jan 12:34 9_bytes
.rw-r--r-- 10Ki cassowary  1 Jan 12:34 10_KiB
.rw-r--r-- 10Mi cassowary  1 Jan 12:34 10_MiB
.rw-r--r-- 10 cassowary  1 Jan 12:34 10_bytes
.rw-r--r-- 11Ki cassowary  1 Jan 12:34 11_KiB
.rw-r--r-- 11Mi cassowary  1 Jan 12:34 11_MiB
.rw-r--r-- 11 cassowary  1 Jan 12:34 11_bytes
.rw-r--r-- 12Ki cassowary  1 Jan 12:34 12_KiB
.rw-r--r-- 12Mi cassowary  1 Jan 12:34 12_MiB
.rw-r--r-- 12 cassowary  1 Jan 12:34 12_bytes
.rw-r--r-- 13Ki cassowary  1 Jan 12:34 13_KiB
.rw-r--r-- 13Mi cassowary  1 Jan 12:34 13_MiB
.rw-r--r-- 13 cassowary  1 Jan 12:34 13_bytes

39
xtests/files_l_bytes Normal file
View File

@ -0,0 +1,39 @@
.rw-r--r-- 1,024 cassowary  1 Jan 12:34 1_KiB
.rw-r--r-- 1,048,576 cassowary  1 Jan 12:34 1_MiB
.rw-r--r-- 1 cassowary  1 Jan 12:34 1_bytes
.rw-r--r-- 2,048 cassowary  1 Jan 12:34 2_KiB
.rw-r--r-- 2,097,152 cassowary  1 Jan 12:34 2_MiB
.rw-r--r-- 2 cassowary  1 Jan 12:34 2_bytes
.rw-r--r-- 3,072 cassowary  1 Jan 12:34 3_KiB
.rw-r--r-- 3,145,728 cassowary  1 Jan 12:34 3_MiB
.rw-r--r-- 3 cassowary  1 Jan 12:34 3_bytes
.rw-r--r-- 4,096 cassowary  1 Jan 12:34 4_KiB
.rw-r--r-- 4,194,304 cassowary  1 Jan 12:34 4_MiB
.rw-r--r-- 4 cassowary  1 Jan 12:34 4_bytes
.rw-r--r-- 5,120 cassowary  1 Jan 12:34 5_KiB
.rw-r--r-- 5,242,880 cassowary  1 Jan 12:34 5_MiB
.rw-r--r-- 5 cassowary  1 Jan 12:34 5_bytes
.rw-r--r-- 6,144 cassowary  1 Jan 12:34 6_KiB
.rw-r--r-- 6,291,456 cassowary  1 Jan 12:34 6_MiB
.rw-r--r-- 6 cassowary  1 Jan 12:34 6_bytes
.rw-r--r-- 7,168 cassowary  1 Jan 12:34 7_KiB
.rw-r--r-- 7,340,032 cassowary  1 Jan 12:34 7_MiB
.rw-r--r-- 7 cassowary  1 Jan 12:34 7_bytes
.rw-r--r-- 8,192 cassowary  1 Jan 12:34 8_KiB
.rw-r--r-- 8,388,608 cassowary  1 Jan 12:34 8_MiB
.rw-r--r-- 8 cassowary  1 Jan 12:34 8_bytes
.rw-r--r-- 9,216 cassowary  1 Jan 12:34 9_KiB
.rw-r--r-- 9,437,184 cassowary  1 Jan 12:34 9_MiB
.rw-r--r-- 9 cassowary  1 Jan 12:34 9_bytes
.rw-r--r-- 10,240 cassowary  1 Jan 12:34 10_KiB
.rw-r--r-- 10,485,760 cassowary  1 Jan 12:34 10_MiB
.rw-r--r-- 10 cassowary  1 Jan 12:34 10_bytes
.rw-r--r-- 11,264 cassowary  1 Jan 12:34 11_KiB
.rw-r--r-- 11,534,336 cassowary  1 Jan 12:34 11_MiB
.rw-r--r-- 11 cassowary  1 Jan 12:34 11_bytes
.rw-r--r-- 12,288 cassowary  1 Jan 12:34 12_KiB
.rw-r--r-- 12,582,912 cassowary  1 Jan 12:34 12_MiB
.rw-r--r-- 12 cassowary  1 Jan 12:34 12_bytes
.rw-r--r-- 13,312 cassowary  1 Jan 12:34 13_KiB
.rw-r--r-- 13,631,488 cassowary  1 Jan 12:34 13_MiB
.rw-r--r-- 13 cassowary  1 Jan 12:34 13_bytes

View File

@ -15,6 +15,11 @@ testcases="/testcases"
results="/vagrant/xtests"
# We want to use strict mode here. Its important that no combination of
# testing flags happens to work by accident!
export EXA_STRICT=1
# Check that no files were created more than a year ago.
# Files not from the current year use a different date format, meaning
# that tests will fail until the VM gets re-provisioned.
@ -56,6 +61,14 @@ COLUMNS=150 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_150
COLUMNS=200 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_200 || exit 1
# File size tests
$exa $testcases/files -l --binary | diff -q - $results/files_l_binary || exit 1
$exa $testcases/files -l --bytes | diff -q - $results/files_l_bytes || exit 1
EXA_STRICT= $exa $testcases/files -l --bytes --binary | diff -q - $results/files_l_binary || exit 1
EXA_STRICT= $exa $testcases/files -l --binary --bytes | diff -q - $results/files_l_bytes || exit 1
# Attributes
# (there are many tests, but theyre all done in one go)
$exa $testcases/attributes -l@T | diff -q - $results/attributes || exit 1
@ -164,6 +177,14 @@ $exa $testcases/hiddens -l -a 2>&1 | diff -q - $results/hiddens_la || exit 1
$exa $testcases/hiddens -l -aa 2>&1 | diff -q - $results/hiddens_laa || exit 1
# Errors
$exa --binary 2>&1 | diff -q - $results/error_useless || exit 1
$exa --ternary 2>&1 | diff -q - $results/error_long || exit 1
$exa -4 2>&1 | diff -q - $results/error_short || exit 1
$exa --time 2>&1 | diff -q - $results/error_value || exit 1
$exa --long=time 2>&1 | diff -q - $results/error_overvalued || exit 1
# And finally...
$exa --help | diff -q - $results/help || exit 1
$exa --help --long | diff -q - $results/help_long || exit 1