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; extern crate lazy_static;
use std::env::var_os;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::io::{stderr, Write, Result as IOResult}; use std::io::{stderr, Write, Result as IOResult};
use std::path::{Component, PathBuf}; use std::path::{Component, PathBuf};
@ -29,7 +30,7 @@ use std::path::{Component, PathBuf};
use ansi_term::{ANSIStrings, Style}; use ansi_term::{ANSIStrings, Style};
use fs::{Dir, File}; use fs::{Dir, File};
use options::Options; use options::{Options, Vars};
pub use options::Misfire; pub use options::Misfire;
use output::{escape, lines, grid, grid_details, details, View, Mode}; 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>, 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> { 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> pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
where I: Iterator<Item=&'args OsString> { 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 } Exa { options, writer, args }
}) })
} }

View File

@ -8,12 +8,12 @@ impl DirAction {
/// Determine which action to perform when trying to list a directory. /// Determine which action to perform when trying to list a directory.
pub fn deduce(matches: &MatchedFlags) -> Result<DirAction, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<DirAction, Misfire> {
let recurse = matches.has(&flags::RECURSE); let recurse = matches.has(&flags::RECURSE)?;
let list = matches.has(&flags::LIST_DIRS); let list = matches.has(&flags::LIST_DIRS)?;
let tree = matches.has(&flags::TREE); let tree = matches.has(&flags::TREE)?;
// Early check for --level when it wouldnt do anything // 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)); return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
} }
@ -37,7 +37,7 @@ impl RecurseOptions {
/// Determine which files should be recursed into. /// Determine which files should be recursed into.
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<RecurseOptions, Misfire> { 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() { match level.to_string_lossy().parse() {
Ok(l) => Some(l), Ok(l) => Some(l),
Err(e) => return Err(Misfire::FailedParse(e)), Err(e) => return Err(Misfire::FailedParse(e)),
@ -55,51 +55,50 @@ impl RecurseOptions {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use std::ffi::OsString;
use options::flags; use options::flags;
use options::parser::Flag;
pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
macro_rules! test { macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => { ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
#[test] #[test]
fn $name() { fn $name() {
use options::parser::{Args, Arg}; use options::parser::Arg;
use std::ffi::OsString; use options::test::parse_for_test;
use options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ]; 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)) {
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>(); assert_eq!(result, $result);
let results = Args(TEST_ARGS).parse(bits.iter()); }
assert_eq!($type::deduce(&results.unwrap().flags), $result);
} }
}; };
} }
// Default behaviour // Default behaviour
test!(empty: DirAction <- [] => Ok(DirAction::List)); test!(empty: DirAction <- []; Both => Ok(DirAction::List));
// Listing files as directories // Listing files as directories
test!(dirs_short: DirAction <- ["-d"] => Ok(DirAction::AsFile)); test!(dirs_short: DirAction <- ["-d"]; Both => Ok(DirAction::AsFile));
test!(dirs_long: DirAction <- ["--list-dirs"] => Ok(DirAction::AsFile)); test!(dirs_long: DirAction <- ["--list-dirs"]; Both => Ok(DirAction::AsFile));
// Recursing // Recursing
test!(rec_short: DirAction <- ["-R"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: None }))); use self::DirAction::Recurse;
test!(rec_long: DirAction <- ["--recurse"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: None }))); test!(rec_short: DirAction <- ["-R"]; Both => Ok(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_long: DirAction <- ["--recurse"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None })));
test!(rec_lim_short_2: DirAction <- ["-RL=5"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(5) }))); test!(rec_lim_short: DirAction <- ["-RL4"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(4) })));
test!(rec_lim_long: DirAction <- ["--recurse", "--level", "666"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(666) }))); test!(rec_lim_short_2: DirAction <- ["-RL=5"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(5) })));
test!(rec_lim_long_2: DirAction <- ["--recurse", "--level=0118"] => Ok(DirAction::Recurse(RecurseOptions { tree: false, max_depth: Some(118) }))); test!(rec_lim_long: DirAction <- ["--recurse", "--level", "666"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(666) })));
test!(rec_tree: DirAction <- ["--recurse", "--tree"] => Ok(DirAction::Recurse(RecurseOptions { tree: true, max_depth: None }))); test!(rec_lim_long_2: DirAction <- ["--recurse", "--level=0118"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(118) })));
test!(rec_short_tree: DirAction <- ["--tree", "--recurse"] => Ok(DirAction::Recurse(RecurseOptions { tree: true, max_depth: None }))); 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 // Errors
test!(error: DirAction <- ["--list-dirs", "--recurse"] => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS))); test!(error: DirAction <- ["--list-dirs", "--recurse"]; Both => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)));
test!(error_2: DirAction <- ["--list-dirs", "--tree"] => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS))); test!(error_2: DirAction <- ["--list-dirs", "--tree"]; Both => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS)));
test!(underwaterlevel: DirAction <- ["--level=4"] => Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))); 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. /// command-line arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> {
Ok(FileFilter { Ok(FileFilter {
list_dirs_first: matches.has(&flags::DIRS_FIRST), list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
reverse: matches.has(&flags::REVERSE), reverse: matches.has(&flags::REVERSE)?,
sort_field: SortField::deduce(matches)?, sort_field: SortField::deduce(matches)?,
dot_filter: DotFilter::deduce(matches)?, dot_filter: DotFilter::deduce(matches)?,
ignore_patterns: IgnorePatterns::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 /// argument. This will return `Err` if the option is there, but does not
/// correspond to a valid field. /// correspond to a valid field.
fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
let word = match matches.get(&flags::SORT) { let word = match matches.get(&flags::SORT)? {
Some(w) => w, Some(w) => w,
None => return Ok(SortField::default()), None => return Ok(SortField::default()),
}; };
@ -85,11 +85,22 @@ impl SortField {
impl DotFilter { impl DotFilter {
pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> { pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> {
match matches.count(&flags::ALL) { let count = matches.count(&flags::ALL);
0 => Ok(DotFilter::JustFiles),
1 => Ok(DotFilter::Dotfiles), if count == 0 {
_ => if matches.has(&flags::TREE) { Err(Misfire::TreeAllAll) } Ok(DotFilter::JustFiles)
else { Ok(DotFilter::DotfilesAndDots) } }
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. /// command-line arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> { 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()), None => return Ok(IgnorePatterns::empty()),
Some(is) => is, Some(is) => is,
}; };
@ -126,6 +137,7 @@ mod test {
use super::*; use super::*;
use std::ffi::OsString; use std::ffi::OsString;
use options::flags; use options::flags;
use options::parser::Flag;
pub fn os(input: &'static str) -> OsString { pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new(); let mut os = OsString::new();
@ -134,17 +146,17 @@ mod test {
} }
macro_rules! test { macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => { ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
#[test] #[test]
fn $name() { fn $name() {
use options::parser::{Args, Arg}; use options::parser::Arg;
use std::ffi::OsString; use options::test::parse_for_test;
use options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB ]; static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB ];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>(); assert_eq!(result, $result);
let results = Args(TEST_ARGS).parse(bits.iter()); }
assert_eq!($type::deduce(&results.unwrap().flags), $result);
} }
}; };
} }
@ -153,21 +165,23 @@ mod test {
use super::*; use super::*;
// Default behaviour // Default behaviour
test!(empty: SortField <- [] => Ok(SortField::default())); test!(empty: SortField <- []; Both => Ok(SortField::default()));
// Sort field arguments // Sort field arguments
test!(one_arg: SortField <- ["--sort=cr"] => Ok(SortField::CreatedDate)); test!(one_arg: SortField <- ["--sort=cr"]; Both => Ok(SortField::CreatedDate));
test!(one_long: SortField <- ["--sort=size"] => Ok(SortField::Size)); test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size));
test!(one_short: SortField <- ["-saccessed"] => Ok(SortField::AccessedDate)); test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
test!(lowercase: SortField <- ["--sort", "name"] => Ok(SortField::Name(SortCase::Sensitive))); test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::Sensitive)));
test!(uppercase: SortField <- ["--sort", "Name"] => Ok(SortField::Name(SortCase::Insensitive))); test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::Insensitive)));
// Errors // 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 // Overriding
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"] => Ok(SortField::ModifiedDate)); test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"] => Ok(SortField::Extension(SortCase::Insensitive))); 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::*; use super::*;
// Default behaviour // Default behaviour
test!(empty: DotFilter <- [] => Ok(DotFilter::JustFiles)); test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles));
// --all // --all
test!(all: DotFilter <- ["--all"] => Ok(DotFilter::Dotfiles)); test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles));
test!(all_all: DotFilter <- ["--all", "-a"] => Ok(DotFilter::DotfilesAndDots)); test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots));
test!(all_all_2: DotFilter <- ["-aa"] => 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 // --all and --tree
test!(tree_a: DotFilter <- ["-Ta"] => Ok(DotFilter::Dotfiles)); test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles));
test!(tree_aa: DotFilter <- ["-Taa"] => Err(Misfire::TreeAllAll)); 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 // Various numbers of globs
test!(none: IgnorePatterns <- [] => Ok(IgnorePatterns::empty())); test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"] => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ]))); test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"] => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ]))); test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
test!(loads: IgnorePatterns <- ["-I*|?|.|*"] => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ]))); 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 /// Determines how to show help, if at all, based on the users
/// command-line arguments. This one works backwards from the other /// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown. /// deduce functions, returning Err if help needs to be shown.
///
/// 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> { pub fn deduce(matches: &MatchedFlags) -> Result<(), HelpString> {
if matches.has(&flags::HELP) { if matches.count(&flags::HELP) > 0 {
let only_long = matches.has(&flags::LONG); let only_long = matches.count(&flags::LONG) > 0;
let git = cfg!(feature="git"); let git = cfg!(feature="git");
let xattrs = xattr::ENABLED; let xattrs = xattr::ENABLED;
Err(HelpString { only_long, git, xattrs }) Err(HelpString { only_long, git, xattrs })
@ -126,21 +130,21 @@ mod test {
#[test] #[test]
fn help() { fn help() {
let args = [ os("--help") ]; let args = [ os("--help") ];
let opts = Options::getopts(&args); let opts = Options::parse(&args, None);
assert!(opts.is_err()) assert!(opts.is_err())
} }
#[test] #[test]
fn help_with_file() { fn help_with_file() {
let args = [ os("--help"), os("me") ]; let args = [ os("--help"), os("me") ];
let opts = Options::getopts(&args); let opts = Options::parse(&args, None);
assert!(opts.is_err()) assert!(opts.is_err())
} }
#[test] #[test]
fn unhelpful() { fn unhelpful() {
let args = []; let args = [];
let opts = Options::getopts(&args); let opts = Options::parse(&args, None);
assert!(opts.is_ok()) // no help when --help isnt passed assert!(opts.is_ok()) // no help when --help isnt passed
} }
} }

View File

@ -5,7 +5,7 @@ use std::num::ParseIntError;
use glob; use glob;
use options::{HelpString, VersionString}; 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 /// A list of legal choices for an argument-taking option
@ -36,6 +36,9 @@ pub enum Misfire {
/// The user wanted the version number. /// The user wanted the version number.
Version(VersionString), Version(VersionString),
/// An option was given twice or more in strict mode.
Duplicate(Flag, Flag),
/// Two options were given that conflict with one another. /// Two options were given that conflict with one another.
Conflict(&'static Arg, &'static Arg), Conflict(&'static Arg, &'static Arg),
@ -89,10 +92,11 @@ impl fmt::Display for Misfire {
match *self { match *self {
BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no value {:?} (Choices: {})", a, b, c), 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), Help(ref text) => write!(f, "{}", text),
Version(ref version) => write!(f, "{}", version), Version(ref version) => write!(f, "{}", version),
Conflict(ref a, ref b) => write!(f, "Option {} conflicts with option {}.", a, b), 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, 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), 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), 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 { 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)] #[allow(unused_results)]
pub fn getopts<'args, I>(args: I) -> Result<(Options, Vec<&'args OsStr>), Misfire> pub fn parse<'args, I, V>(args: I, vars: V) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString> { where I: IntoIterator<Item=&'args OsString>,
use options::parser::Matches; 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, Ok(m) => m,
Err(e) => return Err(Misfire::InvalidOptions(e)), Err(e) => return Err(Misfire::InvalidOptions(e)),
}; };
@ -126,7 +135,7 @@ impl Options {
HelpString::deduce(&flags).map_err(Misfire::Help)?; HelpString::deduce(&flags).map_err(Misfire::Help)?;
VersionString::deduce(&flags).map_err(Misfire::Version)?; VersionString::deduce(&flags).map_err(Misfire::Version)?;
let options = Options::deduce(&flags)?; let options = Options::deduce(&flags, vars)?;
Ok((options, frees)) Ok((options, frees))
} }
@ -136,33 +145,84 @@ impl Options {
pub fn should_scan_for_git(&self) -> bool { pub fn should_scan_for_git(&self) -> bool {
match self.view.mode { match self.view.mode {
Mode::Details(details::Options { table: Some(ref table), .. }) | 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, _ => false,
} }
} }
/// Determines the complete set of options based on the given command-line /// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed. /// arguments, after theyve been parsed.
fn deduce(matches: &MatchedFlags) -> Result<Options, Misfire> { fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<Options, Misfire> {
let dir_action = DirAction::deduce(matches)?; let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?; let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches)?; let view = View::deduce(matches, vars)?;
Ok(Options { dir_action, view, filter }) 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)] #[cfg(test)]
mod test { pub mod test {
use super::{Options, Misfire, flags}; use super::{Options, Misfire, Vars, flags};
use options::parser::{Arg, MatchedFlags};
use std::ffi::OsString; 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) /// Creates an `OSStr` (used in tests)
#[cfg(test)] #[cfg(test)]
fn os(input: &'static str) -> OsString { fn os(input: &str) -> OsString {
let mut os = OsString::new(); let mut os = OsString::new();
os.push(input); os.push(input);
os os
@ -171,106 +231,36 @@ mod test {
#[test] #[test]
fn files() { fn files() {
let args = [ os("this file"), os("that file") ]; 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") ]) assert_eq!(outs, vec![ &os("this file"), &os("that file") ])
} }
#[test] #[test]
fn no_args() { fn no_args() {
let nothing: Vec<OsString> = Vec::new(); 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 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] #[test]
fn long_across() { fn long_across() {
let args = [ os("--long"), os("--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)) assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::LONG))
} }
#[test] #[test]
fn oneline_across() { fn oneline_across() {
let args = [ os("--oneline"), os("--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)) 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] #[test]
#[cfg(feature="git")] #[cfg(feature="git")]
fn just_git() { fn just_git() {
let args = [ os("--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)) 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::ffi::{OsStr, OsString};
use std::fmt; use std::fmt;
use options::Misfire;
/// A **short argument** is a single ASCII character. /// A **short argument** is a single ASCII character.
pub type ShortArg = u8; pub type ShortArg = u8;
@ -50,7 +52,7 @@ pub enum Flag {
} }
impl Flag { impl Flag {
fn matches(&self, arg: &Arg) -> bool { pub fn matches(&self, arg: &Arg) -> bool {
match *self { match *self {
Flag::Short(short) => arg.short == Some(short), Flag::Short(short) => arg.short == Some(short),
Flag::Long(long) => arg.long == long, 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. /// Whether redundant arguments should be considered a problem.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Copy, Clone)]
#[allow(dead_code)] // until strict mode is actually implemented
pub enum Strictness { pub enum Strictness {
/// Throw an error when an argument doesnt do anything, either because /// 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 /// Iterates over the given list of command-line arguments and parses
/// them into a list of matched flags and free strings. /// 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> { where I: IntoIterator<Item=&'args OsString> {
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use self::TakesValue::*; 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)) { match self.0.into_iter().find(|arg| arg.short == Some(short)) {
Some(arg) => Ok(arg), Some(arg) => Ok(arg),
None => Err(ParseError::UnknownShortArgument { attempt: short }) 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) { match self.0.into_iter().find(|arg| arg.long == long) {
Some(arg) => Ok(arg), Some(arg) => Ok(arg),
None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() }) 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 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. /// we need to know where they are in relation to one another.
flags: Vec<(Flag, Option<&'args OsStr>)>, flags: Vec<(Flag, Option<&'args OsStr>)>,
/// Whether to check for duplicate or redundant arguments.
strictness: Strictness,
} }
impl<'a> MatchedFlags<'a> { impl<'a> MatchedFlags<'a> {
/// Whether the given argument was specified. /// Whether the given argument was specified.
pub fn has(&self, arg: &Arg) -> bool { /// Returns `true` if it was, `false` if it wasnt, and an error in
self.flags.iter().rev() /// strict mode if it was specified more than once.
.find(|tuple| tuple.1.is_none() && tuple.0.matches(arg)) pub fn has(&self, arg: &'static Arg) -> Result<bool, Misfire> {
.is_some() self.has_where(|flag| flag.matches(arg)).map(|flag| flag.is_some())
} }
/// If the given argument was specified, return its value. /// Returns the first found argument that satisfies the predicate, or
/// The value is not guaranteed to be valid UTF-8. /// nothing if none is found, or an error in strict mode if multiple
pub fn get(&self, arg: &Arg) -> Option<&OsStr> { /// argument satisfy the predicate.
self.flags.iter().rev() ///
.find(|tuple| tuple.1.is_some() && tuple.0.matches(arg)) /// Youll have to test the resulting flag to see which argument it was.
.map(|tuple| tuple.1.unwrap()) 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 // Its annoying that has and get wont work when accidentally given
// flags that do/dont take values, but this should be caught by tests. // 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 { pub fn count(&self, arg: &Arg) -> usize {
self.flags.iter() self.flags.iter()
.filter(|tuple| tuple.0.matches(arg)) .filter(|tuple| tuple.0.matches(arg))
.count() .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 { mod parse_test {
use super::*; use super::*;
pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
macro_rules! test { macro_rules! test {
($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => { ($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => {
#[test] #[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<OsString> = $frees.as_ref().into_iter().map(|&o| os(o)).collect();
let frees: Vec<&OsStr> = frees.iter().map(|os| os.as_os_str()).collect(); let frees: Vec<&OsStr> = frees.iter().map(|os| os.as_os_str()).collect();
// And again for the flags let flags = <[_]>::into_vec(Box::new($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 got = Args(TEST_ARGS).parse(inputs.iter()); let strictness = Strictness::UseLastArguments; // this isnt even used
let expected = Ok(Matches { frees, flags: MatchedFlags { flags } }); let got = Args(TEST_ARGS).parse(inputs.iter(), strictness);
let expected = Ok(Matches { frees, flags: MatchedFlags { flags, strictness } });
assert_eq!(got, expected); assert_eq!(got, expected);
} }
}; };
@ -463,8 +534,9 @@ mod parse_test {
fn $name() { fn $name() {
use self::ParseError::*; 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 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)); assert_eq!(got, Err($error));
} }
@ -498,8 +570,8 @@ mod parse_test {
// Long args with values // Long args with values
test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") }); test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") });
test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count") }); test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count") });
test!(arg_equals: ["--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("4")) ]); test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]);
// Short args // Short args
@ -511,11 +583,11 @@ mod parse_test {
// Short args with values // Short args with values
test!(bad_short: ["-l=equals"] => error ForbiddenValue { flag: Flag::Short(b'l') }); 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_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_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("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("two")) ]); 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("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("two")) ]); test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
// Unknown args // Unknown args
@ -536,8 +608,12 @@ mod matches_test {
($name:ident: $input:expr, has $param:expr => $result:expr) => { ($name:ident: $input:expr, has $param:expr => $result:expr) => {
#[test] #[test]
fn $name() { fn $name() {
let flags = MatchedFlags { flags: $input.to_vec() }; let flags = MatchedFlags {
assert_eq!(flags.has(&$param), $result); flags: $input.to_vec(),
strictness: Strictness::UseLastArguments,
};
assert_eq!(flags.has(&$param), Ok($result));
} }
}; };
} }
@ -557,8 +633,13 @@ mod matches_test {
#[test] #[test]
fn only_count() { fn only_count() {
let everything = os("everything"); 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] #[test]
@ -568,16 +649,17 @@ mod matches_test {
let flags = MatchedFlags { let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)), 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] #[test]
fn no_count() { 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 /// Determines how to show the version, if at all, based on the users
/// command-line arguments. This one works backwards from the other /// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown. /// deduce functions, returning Err if help needs to be shown.
///
/// Like --help, this doesnt bother checking for errors.
pub fn deduce(matches: &MatchedFlags) -> Result<(), VersionString> { 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") }) Err(VersionString { cargo: env!("CARGO_PKG_VERSION") })
} }
else { else {
@ -52,7 +54,7 @@ mod test {
#[test] #[test]
fn help() { fn help() {
let args = [ os("--version") ]; let args = [ os("--version") ];
let opts = Options::getopts(&args); let opts = Options::parse(&args, None);
assert!(opts.is_err()) assert!(opts.is_err())
} }
} }

View File

@ -1,25 +1,22 @@
use std::env::var_os;
use output::Colours; use output::Colours;
use output::{View, Mode, grid, details}; 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::file_name::{Classify, FileStyle};
use output::time::TimeFormat; use output::time::TimeFormat;
use options::{flags, Misfire}; use options::{flags, Misfire, Vars};
use options::parser::MatchedFlags; use options::parser::MatchedFlags;
use fs::feature::xattr; use fs::feature::xattr;
use info::filetype::FileExtensions; use info::filetype::FileExtensions;
impl View { impl View {
/// Determine which view to use and all of that views arguments. /// Determine which view to use and all of that views arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<View, Misfire> { pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<View, Misfire> {
let mode = Mode::deduce(matches)?; let mode = Mode::deduce(matches, vars)?;
let colours = Colours::deduce(matches)?; let colours = Colours::deduce(matches)?;
let style = FileStyle::deduce(matches); let style = FileStyle::deduce(matches)?;
Ok(View { mode, colours, style }) Ok(View { mode, colours, style })
} }
} }
@ -28,21 +25,21 @@ impl View {
impl Mode { impl Mode {
/// Determine the mode from the command-line arguments. /// Determine the mode from the command-line arguments.
pub fn deduce(matches: &MatchedFlags) -> Result<Mode, Misfire> { pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: V) -> Result<Mode, Misfire> {
use options::misfire::Misfire::*; use options::misfire::Misfire::*;
let long = || { 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)) 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)) Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
} }
else { else {
Ok(details::Options { Ok(details::Options {
table: Some(TableOptions::deduce(matches)?), table: Some(TableOptions::deduce(matches)?),
header: matches.has(&flags::HEADER), header: matches.has(&flags::HEADER)?,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED), xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
}) })
} }
}; };
@ -50,15 +47,15 @@ impl Mode {
let long_options_scan = || { let long_options_scan = || {
for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS, for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] { &flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
if matches.has(option) { if matches.is_strict() && matches.has(option)? {
return Err(Useless(*option, false, &flags::LONG)); 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)) 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)) Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))
} }
else { else {
@ -67,27 +64,27 @@ impl Mode {
}; };
let other_options_scan = || { let other_options_scan = || {
if let Some(width) = TerminalWidth::deduce()?.width() { if let Some(width) = TerminalWidth::deduce(vars)?.width() {
if matches.has(&flags::ONE_LINE) { if matches.has(&flags::ONE_LINE)? {
if matches.has(&flags::ACROSS) { if matches.has(&flags::ACROSS)? {
Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE)) Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE))
} }
else { else {
Ok(Mode::Lines) Ok(Mode::Lines)
} }
} }
else if matches.has(&flags::TREE) { else if matches.has(&flags::TREE)? {
let details = details::Options { let details = details::Options {
table: None, table: None,
header: false, header: false,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED), xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
}; };
Ok(Mode::Details(details)) Ok(Mode::Details(details))
} }
else { else {
let grid = grid::Options { let grid = grid::Options {
across: matches.has(&flags::ACROSS), across: matches.has(&flags::ACROSS)?,
console_width: width, console_width: width,
}; };
@ -99,11 +96,11 @@ impl Mode {
// as the programs stdout being connected to a file, then // as the programs stdout being connected to a file, then
// fallback to the lines view. // fallback to the lines view.
if matches.has(&flags::TREE) { if matches.has(&flags::TREE)? {
let details = details::Options { let details = details::Options {
table: None, table: None,
header: false, header: false,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED), xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
}; };
Ok(Mode::Details(details)) Ok(Mode::Details(details))
@ -114,9 +111,9 @@ impl Mode {
} }
}; };
if matches.has(&flags::LONG) { if matches.has(&flags::LONG)? {
let details = long()?; let details = long()?;
if matches.has(&flags::GRID) { if matches.has(&flags::GRID)? {
match other_options_scan()? { match other_options_scan()? {
Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)), Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)),
others => return Ok(others), others => return Ok(others),
@ -153,8 +150,8 @@ impl TerminalWidth {
/// Determine a requested terminal width from the command-line arguments. /// Determine a requested terminal width from the command-line arguments.
/// ///
/// Returns an error if a requested width doesnt parse to an integer. /// Returns an error if a requested width doesnt parse to an integer.
fn deduce() -> Result<TerminalWidth, Misfire> { fn deduce<V: Vars>(vars: V) -> Result<TerminalWidth, Misfire> {
if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) { if let Some(columns) = vars.get("COLUMNS").and_then(|s| s.into_string().ok()) {
match columns.parse() { match columns.parse() {
Ok(width) => Ok(TerminalWidth::Set(width)), Ok(width) => Ok(TerminalWidth::Set(width)),
Err(e) => Err(Misfire::FailedParse(e)), Err(e) => Err(Misfire::FailedParse(e)),
@ -180,17 +177,26 @@ impl TerminalWidth {
impl TableOptions { impl TableOptions {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
Ok(TableOptions { let env = Environment::load_all();
env: Environment::load_all(), let time_format = TimeFormat::deduce(matches)?;
time_format: TimeFormat::deduce(matches)?, let size_format = SizeFormat::deduce(matches)?;
size_format: SizeFormat::deduce(matches)?, let extra_columns = Columns::deduce(matches)?;
time_types: TimeTypes::deduce(matches)?, Ok(TableOptions { env, time_format, size_format, extra_columns })
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), 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 /// involves the `--binary` or `--bytes` flags, and these conflict with
/// each other. /// each other.
fn deduce(matches: &MatchedFlags) -> Result<SizeFormat, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<SizeFormat, Misfire> {
let binary = matches.has(&flags::BINARY); let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?;
let bytes = matches.has(&flags::BYTES);
match (binary, bytes) { Ok(match flag {
(true, true ) => Err(Misfire::Conflict(&flags::BINARY, &flags::BYTES)), Some(f) if f.matches(&flags::BINARY) => SizeFormat::BinaryBytes,
(true, false) => Ok(SizeFormat::BinaryBytes), Some(f) if f.matches(&flags::BYTES) => SizeFormat::JustBytes,
(false, true ) => Ok(SizeFormat::JustBytes), _ => SizeFormat::DecimalBytes,
(false, false) => Ok(SizeFormat::DecimalBytes), })
}
} }
} }
const TIME_STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
impl TimeFormat { impl TimeFormat {
/// Determine how time should be formatted in timestamp columns. /// Determine how time should be formatted in timestamp columns.
fn deduce(matches: &MatchedFlags) -> Result<TimeFormat, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<TimeFormat, Misfire> {
pub use output::time::{DefaultFormat, ISOFormat}; 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, Some(w) => w,
None => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())), None => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
}; };
@ -244,7 +249,7 @@ impl TimeFormat {
Ok(TimeFormat::FullISO) Ok(TimeFormat::FullISO)
} }
else { 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 /// option, but passing *no* options means that the user just wants to
/// see the default set. /// see the default set.
fn deduce(matches: &MatchedFlags) -> Result<TimeTypes, Misfire> { fn deduce(matches: &MatchedFlags) -> Result<TimeTypes, Misfire> {
let possible_word = matches.get(&flags::TIME); let possible_word = matches.get(&flags::TIME)?;
let modified = matches.has(&flags::MODIFIED); let modified = matches.has(&flags::MODIFIED)?;
let created = matches.has(&flags::CREATED); let created = matches.has(&flags::CREATED)?;
let accessed = matches.has(&flags::ACCESSED); let accessed = matches.has(&flags::ACCESSED)?;
if let Some(word) = possible_word { if let Some(word) = possible_word {
if modified { if modified {
@ -329,13 +334,14 @@ impl Default for TerminalColours {
} }
} }
const COLOURS: &[&str] = &["always", "auto", "never"];
impl TerminalColours { impl TerminalColours {
/// Determine which terminal colour conditions to use. /// Determine which terminal colour conditions to use.
fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> { 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, Some(w) => w,
None => return Ok(TerminalColours::default()), None => return Ok(TerminalColours::default()),
}; };
@ -362,7 +368,7 @@ impl Colours {
let tc = TerminalColours::deduce(matches)?; let tc = TerminalColours::deduce(matches)?;
if tc == Always || (tc == Automatic && TERM_WIDTH.is_some()) { 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)) Ok(Colours::colourful(scale))
} }
else { else {
@ -374,17 +380,19 @@ impl Colours {
impl FileStyle { impl FileStyle {
fn deduce(matches: &MatchedFlags) -> FileStyle { fn deduce(matches: &MatchedFlags) -> Result<FileStyle, Misfire> {
let classify = Classify::deduce(matches); let classify = Classify::deduce(matches)?;
let exts = FileExtensions; let exts = FileExtensions;
FileStyle { classify, exts } Ok(FileStyle { classify, exts })
} }
} }
impl Classify { impl Classify {
fn deduce(matches: &MatchedFlags) -> Classify { fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
if matches.has(&flags::CLASSIFY) { Classify::AddFileIndicators } let flagged = matches.has(&flags::CLASSIFY)?;
else { Classify::JustFilenames }
Ok(if flagged { Classify::AddFileIndicators }
else { Classify::JustFilenames })
} }
} }
@ -409,6 +417,10 @@ mod test {
use super::*; use super::*;
use std::ffi::OsString; use std::ffi::OsString;
use options::flags; 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 { pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new(); let mut os = OsString::new();
@ -416,19 +428,74 @@ mod test {
os 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 { 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] #[test]
fn $name() { fn $name() {
use options::parser::{Args, Arg}; for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
use std::ffi::OsString; assert_eq!(result, $result);
}
}
};
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => {
&flags::TIME, &flags::MODIFIED, &flags::CREATED, &flags::ACCESSED ]; /// 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>>(); ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => like $pat:pat) => {
let results = Args(TEST_ARGS).parse(bits.iter()); /// More general macro for testing against a pattern.
assert_eq!($type::deduce(&results.unwrap().flags), $result); /// 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 { mod size_formats {
use super::*; use super::*;
test!(empty: SizeFormat <- [] => Ok(SizeFormat::DecimalBytes)); // Default behaviour
test!(binary: SizeFormat <- ["--binary"] => Ok(SizeFormat::BinaryBytes)); test!(empty: SizeFormat <- []; Both => Ok(SizeFormat::DecimalBytes));
test!(bytes: SizeFormat <- ["--bytes"] => Ok(SizeFormat::JustBytes));
test!(both: SizeFormat <- ["--binary", "--bytes"] => Err(Misfire::Conflict(&flags::BINARY, &flags::BYTES))); // 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::*; use super::*;
// Default behaviour // Default behaviour
test!(empty: TimeTypes <- [] => Ok(TimeTypes::default())); test!(empty: TimeTypes <- []; Both => 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!(acc: TimeTypes <- ["--accessed"] => Ok(TimeTypes { accessed: true, modified: false, created: false })); // Modified
test!(a: TimeTypes <- ["-u"] => Ok(TimeTypes { accessed: true, modified: false, created: false })); test!(modified: TimeTypes <- ["--modified"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_acc: TimeTypes <- ["--time", "accessed"] => Ok(TimeTypes { accessed: true, modified: false, created: false })); test!(m: TimeTypes <- ["-m"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false }));
test!(time_a: TimeTypes <- ["-t", "acc"] => Ok(TimeTypes { accessed: true, modified: false, 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 })); // Accessed
test!(c: TimeTypes <- ["-U"] => Ok(TimeTypes { accessed: false, modified: false, created: true })); test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(time_cr: TimeTypes <- ["--time=created"] => Ok(TimeTypes { accessed: false, modified: false, created: true })); test!(a: TimeTypes <- ["-u"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false }));
test!(time_c: TimeTypes <- ["-tcr"] => Ok(TimeTypes { accessed: false, modified: false, created: true })); 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 // Multiples
test!(time_uu: TimeTypes <- ["-uU"] => Ok(TimeTypes { accessed: true, modified: false, created: true })); test!(time_uu: TimeTypes <- ["-uU"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: true }));
// Overriding
test!(time_mc: TimeTypes <- ["-tcr", "-tmod"] => Ok(TimeTypes { accessed: false, modified: true, created: false }));
// Errors // Errors
test!(time_tea: TimeTypes <- ["--time=tea"] => Err(Misfire::bad_argument(&flags::TIME, &os("tea"), super::TIMES))); test!(time_tea: TimeTypes <- ["--time=tea"]; Both => 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_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 env: Environment,
pub size_format: SizeFormat, pub size_format: SizeFormat,
pub time_format: TimeFormat, 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, pub time_types: TimeTypes,
// The rest are just on/off
pub inode: bool, pub inode: bool,
pub links: bool, pub links: bool,
pub blocks: 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 { pub fn should_scan_for_git(&self) -> bool {
self.git self.git
} }
@ -57,7 +67,7 @@ impl Options {
columns.push(Column::HardLinks); columns.push(Column::HardLinks);
} }
columns.push(Column::FileSize(self.size_format)); columns.push(Column::FileSize);
if self.blocks { if self.blocks {
columns.push(Column::Blocks); columns.push(Column::Blocks);
@ -98,7 +108,7 @@ impl Options {
#[derive(Debug)] #[derive(Debug)]
pub enum Column { pub enum Column {
Permissions, Permissions,
FileSize(SizeFormat), FileSize,
Timestamp(TimeType), Timestamp(TimeType),
Blocks, Blocks,
User, User,
@ -120,7 +130,7 @@ impl Column {
/// Get the alignment this column should use. /// Get the alignment this column should use.
pub fn alignment(&self) -> Alignment { pub fn alignment(&self) -> Alignment {
match *self { match *self {
Column::FileSize(_) Column::FileSize
| Column::HardLinks | Column::HardLinks
| Column::Inode | Column::Inode
| Column::Blocks | Column::Blocks
@ -134,7 +144,7 @@ impl Column {
pub fn header(&self) -> &'static str { pub fn header(&self) -> &'static str {
match *self { match *self {
Column::Permissions => "Permissions", Column::Permissions => "Permissions",
Column::FileSize(_) => "Size", Column::FileSize => "Size",
Column::Timestamp(t) => t.header(), Column::Timestamp(t) => t.header(),
Column::Blocks => "Blocks", Column::Blocks => "Blocks",
Column::User => "User", Column::User => "User",
@ -276,6 +286,7 @@ pub struct Table<'a> {
env: &'a Environment, env: &'a Environment,
widths: TableWidths, widths: TableWidths,
time_format: &'a TimeFormat, time_format: &'a TimeFormat,
size_format: SizeFormat,
} }
#[derive(Clone)] #[derive(Clone)]
@ -285,9 +296,14 @@ pub struct Row {
impl<'a, 'f> Table<'a> { impl<'a, 'f> Table<'a> {
pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> 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()); 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 { pub fn widths(&self) -> &TableWidths {
@ -327,7 +343,7 @@ impl<'a, 'f> Table<'a> {
match *column { match *column {
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours), 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::HardLinks => file.links().render(&self.colours, &self.env.numeric),
Column::Inode => file.inode().render(&self.colours), Column::Inode => file.inode().render(&self.colours),
Column::Blocks => file.blocks().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::{LocalDateTime, TimeZone, DatePiece, TimePiece};
use datetime::fmt::DateFormat; use datetime::fmt::DateFormat;
use locale; use locale;
@ -6,13 +8,48 @@ use std::cmp;
use fs::fields::Time; 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 { 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(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), 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, 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, FullISO,
} }
// There are two different formatting functions because local and zoned
// timestamps are separate types.
impl TimeFormat { impl TimeFormat {
pub fn format_local(&self, time: Time) -> String { pub fn format_local(&self, time: Time) -> String {
match *self { 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" 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. # Check that no files were created more than a year ago.
# Files not from the current year use a different date format, meaning # Files not from the current year use a different date format, meaning
# that tests will fail until the VM gets re-provisioned. # 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 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 # Attributes
# (there are many tests, but theyre all done in one go) # (there are many tests, but theyre all done in one go)
$exa $testcases/attributes -l@T | diff -q - $results/attributes || exit 1 $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 $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... # And finally...
$exa --help | diff -q - $results/help || exit 1 $exa --help | diff -q - $results/help || exit 1
$exa --help --long | diff -q - $results/help_long || exit 1 $exa --help --long | diff -q - $results/help_long || exit 1