Merge remote-tracking branch origin/option-pars-ng

This merges in exa’s own new options parser, which has the following features:

- You can specify an option twice and it’ll use the second one, making aliases usable for defaults (fixes #144)
- Lets arguments be specified more than once (fixes #125)

Strict mode is not done yet; I just wanted to merge this in because it’s been a while, and there’s work that needs to be done on master so I don’t want them drifting apart any further.

It’s likely that you’ll find cases where multiple arguments doesn’t work or where the wrong value is being used. There aren’t tests for *everything* yet, and it still uses global environment variables.

# Conflicts:
#	src/options/view.rs
This commit is contained in:
Benjamin Sago 2017-08-05 21:25:00 +01:00
commit b5bcf22612
20 changed files with 1678 additions and 636 deletions

View File

@ -16,6 +16,7 @@ license = "MIT"
[[bin]]
name = "exa"
path = "src/bin/main.rs"
doc = false
[lib]
name = "exa"

4
Vagrantfile vendored
View File

@ -59,8 +59,8 @@ Vagrant.configure(2) do |config|
config.vm.provision :shell, privileged: true, inline: <<-EOF
set -xe
echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \\$*" > /usr/bin/exa
echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \\$*" > /usr/bin/rexa
echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \"\\$*\"" > /usr/bin/exa
echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \"\\$*\"" > /usr/bin/rexa
chmod +x /usr/bin/{exa,rexa}
EOF

View File

@ -1,14 +1,15 @@
extern crate exa;
use exa::Exa;
use std::ffi::OsString;
use std::env::args_os;
use std::io::{stdout, stderr, Write, ErrorKind};
use std::process::exit;
fn main() {
let args = args_os().skip(1);
match Exa::new(args, &mut stdout()) {
let args: Vec<OsString> = args_os().skip(1).collect();
match Exa::new(args.iter(), &mut stdout()) {
Ok(mut exa) => {
match exa.run() {
Ok(exit_status) => exit(exit_status),

View File

@ -3,7 +3,6 @@
extern crate ansi_term;
extern crate datetime;
extern crate getopts;
extern crate glob;
extern crate libc;
extern crate locale;
@ -23,16 +22,16 @@ extern crate term_size;
extern crate lazy_static;
use std::ffi::OsStr;
use std::ffi::{OsStr, OsString};
use std::io::{stderr, Write, Result as IOResult};
use std::path::{Component, PathBuf};
use ansi_term::{ANSIStrings, Style};
use fs::{Dir, File};
use options::{Options, View, Mode};
use options::Options;
pub use options::Misfire;
use output::{escape, lines, grid, grid_details, details};
use output::{escape, lines, grid, grid_details, details, View, Mode};
mod fs;
mod info;
@ -41,7 +40,7 @@ mod output;
/// The main program wrapper.
pub struct Exa<'w, W: Write + 'w> {
pub struct Exa<'args, 'w, W: Write + 'w> {
/// List of command-line options, having been successfully parsed.
pub options: Options,
@ -53,12 +52,12 @@ pub struct Exa<'w, W: Write + 'w> {
/// List of the free command-line arguments that should correspond to file
/// names (anything that isnt an option).
pub args: Vec<String>,
pub args: Vec<&'args OsStr>,
}
impl<'w, W: Write + 'w> Exa<'w, W> {
pub fn new<C>(args: C, writer: &'w mut W) -> Result<Exa<'w, W>, Misfire>
where C: IntoIterator, C::Item: AsRef<OsStr> {
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)| {
Exa { options, writer, args }
})
@ -71,20 +70,20 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
// List the current directory by default, like ls.
if self.args.is_empty() {
self.args.push(".".to_owned());
self.args = vec![ OsStr::new(".") ];
}
for file_name in &self.args {
match File::new(PathBuf::from(file_name), None, None) {
for file_path in &self.args {
match File::new(PathBuf::from(file_path), None, None) {
Err(e) => {
exit_status = 2;
writeln!(stderr(), "{}: {}", file_name, e)?;
writeln!(stderr(), "{:?}: {}", file_path, e)?;
},
Ok(f) => {
if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() {
match f.to_dir(self.options.should_scan_for_git()) {
Ok(d) => dirs.push(d),
Err(e) => writeln!(stderr(), "{}: {}", file_name, e)?,
Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?,
}
}
else {

64
src/fs/dir_action.rs Normal file
View File

@ -0,0 +1,64 @@
/// What to do when encountering a directory?
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum DirAction {
/// This directory should be listed along with the regular files, instead
/// of having its contents queried.
AsFile,
/// This directory should not be listed, and should instead be opened and
/// *its* files listed separately. This is the default behaviour.
List,
/// This directory should be listed along with the regular files, and then
/// its contents should be listed afterward. The recursive contents of
/// *those* contents are dictated by the options argument.
Recurse(RecurseOptions),
}
impl DirAction {
/// Gets the recurse options, if this dir action has any.
pub fn recurse_options(&self) -> Option<RecurseOptions> {
match *self {
DirAction::Recurse(opts) => Some(opts),
_ => None,
}
}
/// Whether to treat directories as regular files or not.
pub fn treat_dirs_as_files(&self) -> bool {
match *self {
DirAction::AsFile => true,
DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
_ => false,
}
}
}
/// The options that determine how to recurse into a directory.
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct RecurseOptions {
/// Whether recursion should be done as a tree or as multiple individual
/// views of files.
pub tree: bool,
/// The maximum number of times that recursion should descend to, if one
/// is specified.
pub max_depth: Option<usize>,
}
impl RecurseOptions {
/// Returns whether a directory of the given depth would be too deep.
pub fn is_too_deep(&self, depth: usize) -> bool {
match self.max_depth {
None => false,
Some(d) => {
d <= depth
}
}
}
}

221
src/fs/filter.rs Normal file
View File

@ -0,0 +1,221 @@
use std::cmp::Ordering;
use std::os::unix::fs::MetadataExt;
use glob;
use natord;
use fs::File;
use fs::DotFilter;
/// The **file filter** processes a vector of files before outputting them,
/// filtering and sorting the files depending on the users command-line
/// flags.
#[derive(PartialEq, Debug, Clone)]
pub struct FileFilter {
/// Whether directories should be listed first, and other types of file
/// second. Some users prefer it like this.
pub list_dirs_first: bool,
/// The metadata field to sort by.
pub sort_field: SortField,
/// Whether to reverse the sorting order. This would sort the largest
/// files first, or files starting with Z, or the most-recently-changed
/// ones, depending on the sort field.
pub reverse: bool,
/// Which invisible “dot” files to include when listing a directory.
///
/// Files starting with a single “.” are used to determine “system” or
/// “configuration” files that should not be displayed in a regular
/// directory listing, and the directory entries “.” and “..” are
/// considered extra-special.
///
/// This came about more or less by a complete historical accident,
/// when the original `ls` tried to hide `.` and `..`:
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
///
/// When one typed ls, however, these files appeared, so either Ken or
/// Dennis added a simple test to the program. It was in assembler then,
/// but the code in question was equivalent to something like this:
/// if (name[0] == '.') continue;
/// This statement was a little shorter than what it should have been,
/// which is:
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
/// but hey, it was easy.
///
/// Two things resulted.
///
/// First, a bad precedent was set. A lot of other lazy programmers
/// introduced bugs by making the same simplification. Actual files
/// beginning with periods are often skipped when they should be counted.
///
/// Second, and much worse, the idea of a "hidden" or "dot" file was
/// created. As a consequence, more lazy programmers started dropping
/// files into everyone's home directory. I don't have all that much
/// stuff installed on the machine I'm using to type this, but my home
/// directory has about a hundred dot files and I don't even know what
/// most of them are or whether they're still needed. Every file name
/// evaluation that goes through my home directory is slowed down by
/// this accumulated sludge.
pub dot_filter: DotFilter,
/// Glob patterns to ignore. Any file name that matches *any* of these
/// patterns won't be displayed in the list.
pub ignore_patterns: IgnorePatterns,
}
impl FileFilter {
/// Remove every file in the given vector that does *not* pass the
/// filter predicate for files found inside a directory.
pub fn filter_child_files(&self, files: &mut Vec<File>) {
files.retain(|f| !self.ignore_patterns.is_ignored(f));
}
/// Remove every file in the given vector that does *not* pass the
/// filter predicate for file names specified on the command-line.
///
/// The rules are different for these types of files than the other
/// type because the ignore rules can be used with globbing. For
/// example, running "exa -I='*.tmp' .vimrc" shouldn't filter out the
/// dotfile, because it's been directly specified. But running
/// "exa -I='*.ogg' music/*" should filter out the ogg files obtained
/// from the glob, even though the globbing is done by the shell!
pub fn filter_argument_files(&self, files: &mut Vec<File>) {
files.retain(|f| !self.ignore_patterns.is_ignored(f));
}
/// Sort the files in the given vector based on the sort field option.
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
where F: AsRef<File<'a>> {
files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
if self.reverse {
files.reverse();
}
if self.list_dirs_first {
// This relies on the fact that `sort_by` is stable.
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
}
}
/// Compares two files to determine the order they should be listed in,
/// depending on the search field.
pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
use self::SortCase::{Sensitive, Insensitive};
match self.sort_field {
SortField::Unsorted => Ordering::Equal,
SortField::Name(Sensitive) => natord::compare(&a.name, &b.name),
SortField::Name(Insensitive) => natord::compare_ignore_case(&a.name, &b.name),
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes
Ordering::Equal => natord::compare(&*a.name, &*b.name),
order => order,
},
SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare(&*a.name, &*b.name),
order => order,
},
SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
order => order,
},
}
}
}
/// User-supplied field to sort by.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortField {
/// Don't apply any sorting. This is usually used as an optimisation in
/// scripts, where the order doesn't matter.
Unsorted,
/// The file name. This is the default sorting.
Name(SortCase),
/// The file's extension, with extensionless files being listed first.
Extension(SortCase),
/// The file's size.
Size,
/// The file's inode. This is sometimes analogous to the order in which
/// the files were created on the hard drive.
FileInode,
/// The time at which this file was modified (the `mtime`).
///
/// As this is stored as a Unix timestamp, rather than a local time
/// instance, the time zone does not matter and will only be used to
/// display the timestamps, not compare them.
ModifiedDate,
/// The time at this file was accessed (the `atime`).
///
/// Oddly enough, this field rarely holds the *actual* accessed time.
/// Recording a read time means writing to the file each time its read
/// slows the whole operation down, so many systems will only update the
/// timestamp in certain circumstances. This has become common enough that
/// its now expected behaviour for the `atime` field.
/// http://unix.stackexchange.com/a/8842
AccessedDate,
/// The time at which this file was changed or created (the `ctime`).
///
/// Contrary to the name, this field is used to mark the time when a
/// file's metadata changed -- its permissions, owners, or link count.
///
/// In original Unix, this was, however, meant as creation time.
/// https://www.bell-labs.com/usr/dmr/www/cacm.html
CreatedDate,
/// The type of the file: directories, links, pipes, regular, files, etc.
///
/// Files are ordered according to the `PartialOrd` implementation of
/// `fs::fields::Type`, so changing that will change this.
FileType,
}
/// Whether a field should be sorted case-sensitively or case-insensitively.
///
/// This determines which of the `natord` functions to use.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortCase {
/// Sort files case-sensitively with uppercase first, with A coming
/// before a.
Sensitive,
/// Sort files case-insensitively, with A being equal to a.
Insensitive,
}
#[derive(PartialEq, Default, Debug, Clone)]
pub struct IgnorePatterns {
pub patterns: Vec<glob::Pattern>,
}
impl IgnorePatterns {
fn is_ignored(&self, file: &File) -> bool {
self.patterns.iter().any(|p| p.matches(&file.name))
}
}

View File

@ -6,3 +6,5 @@ pub use self::file::{File, FileTarget};
pub mod feature;
pub mod fields;
pub mod filter;
pub mod dir_action;

View File

@ -1,40 +1,28 @@
use getopts;
use options::parser::MatchedFlags;
use options::{flags, Misfire};
use options::misfire::Misfire;
use fs::dir_action::{DirAction, RecurseOptions};
/// What to do when encountering a directory?
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum DirAction {
/// This directory should be listed along with the regular files, instead
/// of having its contents queried.
AsFile,
/// This directory should not be listed, and should instead be opened and
/// *its* files listed separately. This is the default behaviour.
List,
/// This directory should be listed along with the regular files, and then
/// its contents should be listed afterward. The recursive contents of
/// *those* contents are dictated by the options argument.
Recurse(RecurseOptions),
}
impl DirAction {
/// Determine which action to perform when trying to list a directory.
pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
let recurse = matches.opt_present("recurse");
let list = matches.opt_present("list-dirs");
let tree = matches.opt_present("tree");
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);
// Early check for --level when it wouldnt do anything
if !recurse && !tree && matches.get(&flags::LEVEL).is_some() {
return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
}
match (recurse, list, tree) {
// You can't --list-dirs along with --recurse or --tree because
// they already automatically list directories.
(true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
(_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
(true, true, _ ) => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)),
(_, true, true ) => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS)),
(_ , _, true ) => Ok(DirAction::Recurse(RecurseOptions::deduce(matches, true)?)),
(true, false, false) => Ok(DirAction::Recurse(RecurseOptions::deduce(matches, false)?)),
@ -42,45 +30,15 @@ impl DirAction {
(false, false, _ ) => Ok(DirAction::List),
}
}
/// Gets the recurse options, if this dir action has any.
pub fn recurse_options(&self) -> Option<RecurseOptions> {
match *self {
DirAction::Recurse(opts) => Some(opts),
_ => None,
}
}
/// Whether to treat directories as regular files or not.
pub fn treat_dirs_as_files(&self) -> bool {
match *self {
DirAction::AsFile => true,
DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
_ => false,
}
}
}
/// The options that determine how to recurse into a directory.
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct RecurseOptions {
/// Whether recursion should be done as a tree or as multiple individual
/// views of files.
pub tree: bool,
/// The maximum number of times that recursion should descend to, if one
/// is specified.
pub max_depth: Option<usize>,
}
impl RecurseOptions {
/// Determine which files should be recursed into.
pub fn deduce(matches: &getopts::Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
let max_depth = if let Some(level) = matches.opt_str("level") {
match level.parse() {
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<RecurseOptions, Misfire> {
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)),
}
@ -91,14 +49,57 @@ impl RecurseOptions {
Ok(RecurseOptions { tree, max_depth })
}
}
/// Returns whether a directory of the given depth would be too deep.
pub fn is_too_deep(&self, depth: usize) -> bool {
match self.max_depth {
None => false,
Some(d) => {
d <= depth
}
}
#[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
}
}
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => {
#[test]
fn $name() {
use options::parser::{Args, Arg};
use std::ffi::OsString;
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);
}
};
}
// Default behaviour
test!(empty: DirAction <- [] => Ok(DirAction::List));
// Listing files as directories
test!(dirs_short: DirAction <- ["-d"] => Ok(DirAction::AsFile));
test!(dirs_long: DirAction <- ["--list-dirs"] => 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 })));
// 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)));
}

View File

@ -1,226 +1,28 @@
use std::cmp::Ordering;
use std::os::unix::fs::MetadataExt;
use getopts;
use glob;
use natord;
use fs::File;
use fs::DotFilter;
use options::misfire::Misfire;
use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns};
use options::{flags, Misfire};
use options::parser::MatchedFlags;
/// The **file filter** processes a vector of files before outputting them,
/// filtering and sorting the files depending on the users command-line
/// flags.
#[derive(Default, PartialEq, Debug, Clone)]
pub struct FileFilter {
/// Whether directories should be listed first, and other types of file
/// second. Some users prefer it like this.
pub list_dirs_first: bool,
/// The metadata field to sort by.
pub sort_field: SortField,
/// Whether to reverse the sorting order. This would sort the largest
/// files first, or files starting with Z, or the most-recently-changed
/// ones, depending on the sort field.
pub reverse: bool,
/// Which invisible “dot” files to include when listing a directory.
///
/// Files starting with a single “.” are used to determine “system” or
/// “configuration” files that should not be displayed in a regular
/// directory listing, and the directory entries “.” and “..” are
/// considered extra-special.
///
/// This came about more or less by a complete historical accident,
/// when the original `ls` tried to hide `.` and `..`:
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
///
/// When one typed ls, however, these files appeared, so either Ken or
/// Dennis added a simple test to the program. It was in assembler then,
/// but the code in question was equivalent to something like this:
/// if (name[0] == '.') continue;
/// This statement was a little shorter than what it should have been,
/// which is:
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
/// but hey, it was easy.
///
/// Two things resulted.
///
/// First, a bad precedent was set. A lot of other lazy programmers
/// introduced bugs by making the same simplification. Actual files
/// beginning with periods are often skipped when they should be counted.
///
/// Second, and much worse, the idea of a "hidden" or "dot" file was
/// created. As a consequence, more lazy programmers started dropping
/// files into everyone's home directory. I don't have all that much
/// stuff installed on the machine I'm using to type this, but my home
/// directory has about a hundred dot files and I don't even know what
/// most of them are or whether they're still needed. Every file name
/// evaluation that goes through my home directory is slowed down by
/// this accumulated sludge.
pub dot_filter: DotFilter,
/// Glob patterns to ignore. Any file name that matches *any* of these
/// patterns won't be displayed in the list.
ignore_patterns: IgnorePatterns,
}
impl FileFilter {
/// Determines the set of file filter options to use, based on the users
/// command-line arguments.
pub fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> {
Ok(FileFilter {
list_dirs_first: matches.opt_present("group-directories-first"),
reverse: matches.opt_present("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)?,
})
}
/// Remove every file in the given vector that does *not* pass the
/// filter predicate for files found inside a directory.
pub fn filter_child_files(&self, files: &mut Vec<File>) {
files.retain(|f| !self.ignore_patterns.is_ignored(f));
}
/// Remove every file in the given vector that does *not* pass the
/// filter predicate for file names specified on the command-line.
///
/// The rules are different for these types of files than the other
/// type because the ignore rules can be used with globbing. For
/// example, running "exa -I='*.tmp' .vimrc" shouldn't filter out the
/// dotfile, because it's been directly specified. But running
/// "exa -I='*.ogg' music/*" should filter out the ogg files obtained
/// from the glob, even though the globbing is done by the shell!
pub fn filter_argument_files(&self, files: &mut Vec<File>) {
files.retain(|f| !self.ignore_patterns.is_ignored(f));
}
/// Sort the files in the given vector based on the sort field option.
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
where F: AsRef<File<'a>> {
files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
if self.reverse {
files.reverse();
}
if self.list_dirs_first {
// This relies on the fact that `sort_by` is stable.
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
}
}
/// Compares two files to determine the order they should be listed in,
/// depending on the search field.
pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
use self::SortCase::{Sensitive, Insensitive};
match self.sort_field {
SortField::Unsorted => Ordering::Equal,
SortField::Name(Sensitive) => natord::compare(&a.name, &b.name),
SortField::Name(Insensitive) => natord::compare_ignore_case(&a.name, &b.name),
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes
Ordering::Equal => natord::compare(&*a.name, &*b.name),
order => order,
},
SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare(&*a.name, &*b.name),
order => order,
},
SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
order => order,
},
}
}
}
/// User-supplied field to sort by.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortField {
/// Don't apply any sorting. This is usually used as an optimisation in
/// scripts, where the order doesn't matter.
Unsorted,
/// The file name. This is the default sorting.
Name(SortCase),
/// The file's extension, with extensionless files being listed first.
Extension(SortCase),
/// The file's size.
Size,
/// The file's inode. This is sometimes analogous to the order in which
/// the files were created on the hard drive.
FileInode,
/// The time at which this file was modified (the `mtime`).
///
/// As this is stored as a Unix timestamp, rather than a local time
/// instance, the time zone does not matter and will only be used to
/// display the timestamps, not compare them.
ModifiedDate,
/// The time at this file was accessed (the `atime`).
///
/// Oddly enough, this field rarely holds the *actual* accessed time.
/// Recording a read time means writing to the file each time its read
/// slows the whole operation down, so many systems will only update the
/// timestamp in certain circumstances. This has become common enough that
/// its now expected behaviour for the `atime` field.
/// http://unix.stackexchange.com/a/8842
AccessedDate,
/// The time at which this file was changed or created (the `ctime`).
///
/// Contrary to the name, this field is used to mark the time when a
/// file's metadata changed -- its permissions, owners, or link count.
///
/// In original Unix, this was, however, meant as creation time.
/// https://www.bell-labs.com/usr/dmr/www/cacm.html
CreatedDate,
/// The type of the file: directories, links, pipes, regular, files, etc.
///
/// Files are ordered according to the `PartialOrd` implementation of
/// `fs::fields::Type`, so changing that will change this.
FileType,
}
/// Whether a field should be sorted case-sensitively or case-insensitively.
///
/// This determines which of the `natord` functions to use.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortCase {
/// Sort files case-sensitively with uppercase first, with A coming
/// before a.
Sensitive,
/// Sort files case-insensitively, with A being equal to a.
Insensitive,
}
impl Default for SortField {
fn default() -> SortField {
@ -228,78 +30,171 @@ impl Default for SortField {
}
}
const SORTS: &[&str] = &[ "name", "Name", "size", "extension",
"Extension", "modified", "accessed",
"created", "inode", "type", "none" ];
impl SortField {
/// Determine the sort field to use, based on the presence of a “sort”
/// argument. This will return `Err` if the option is there, but does not
/// correspond to a valid field.
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
let word = match matches.get(&flags::SORT) {
Some(w) => w,
None => return Ok(SortField::default()),
};
const SORTS: &[&str] = &[ "name", "Name", "size", "extension",
"Extension", "modified", "accessed",
"created", "inode", "type", "none" ];
if let Some(word) = matches.opt_str("sort") {
match &*word {
"name" | "filename" => Ok(SortField::Name(SortCase::Sensitive)),
"Name" | "Filename" => Ok(SortField::Name(SortCase::Insensitive)),
"size" | "filesize" => Ok(SortField::Size),
"ext" | "extension" => Ok(SortField::Extension(SortCase::Sensitive)),
"Ext" | "Extension" => Ok(SortField::Extension(SortCase::Insensitive)),
"mod" | "modified" => Ok(SortField::ModifiedDate),
"acc" | "accessed" => Ok(SortField::AccessedDate),
"cr" | "created" => Ok(SortField::CreatedDate),
"inode" => Ok(SortField::FileInode),
"type" => Ok(SortField::FileType),
"none" => Ok(SortField::Unsorted),
field => Err(Misfire::bad_argument("sort", field, SORTS))
}
if word == "name" || word == "filename" {
Ok(SortField::Name(SortCase::Sensitive))
}
else if word == "Name" || word == "Filename" {
Ok(SortField::Name(SortCase::Insensitive))
}
else if word == "size" || word == "filesize" {
Ok(SortField::Size)
}
else if word == "ext" || word == "extension" {
Ok(SortField::Extension(SortCase::Sensitive))
}
else if word == "Ext" || word == "Extension" {
Ok(SortField::Extension(SortCase::Insensitive))
}
else if word == "mod" || word == "modified" {
Ok(SortField::ModifiedDate)
}
else if word == "acc" || word == "accessed" {
Ok(SortField::AccessedDate)
}
else if word == "cr" || word == "created" {
Ok(SortField::CreatedDate)
}
else if word == "inode" {
Ok(SortField::FileInode)
}
else if word == "type" {
Ok(SortField::FileType)
}
else if word == "none" {
Ok(SortField::Unsorted)
}
else {
Ok(SortField::default())
Err(Misfire::bad_argument(&flags::SORT, word, SORTS))
}
}
}
impl DotFilter {
pub fn deduce(matches: &getopts::Matches) -> Result<DotFilter, Misfire> {
let dots = match matches.opt_count("all") {
0 => return Ok(DotFilter::JustFiles),
1 => DotFilter::Dotfiles,
_ => DotFilter::DotfilesAndDots,
};
if matches.opt_present("tree") {
Err(Misfire::Useless("all --all", true, "tree"))
}
else {
Ok(dots)
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) }
}
}
}
#[derive(PartialEq, Default, Debug, Clone)]
struct IgnorePatterns {
patterns: Vec<glob::Pattern>,
}
impl IgnorePatterns {
/// Determines the set of file filter options to use, based on the users
/// command-line arguments.
pub fn deduce(matches: &getopts::Matches) -> Result<IgnorePatterns, Misfire> {
let patterns = match matches.opt_str("ignore-glob") {
pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> {
let patterns = match matches.get(&flags::IGNORE_GLOB) {
None => Ok(Vec::new()),
Some(is) => is.split('|').map(|a| glob::Pattern::new(a)).collect(),
};
Some(is) => is.to_string_lossy().split('|').map(|a| glob::Pattern::new(a)).collect(),
}?;
Ok(IgnorePatterns {
patterns: patterns?,
})
}
// TODO: is to_string_lossy really the best way to handle
// invalid UTF-8 there?
fn is_ignored(&self, file: &File) -> bool {
self.patterns.iter().any(|p| p.matches(&file.name))
Ok(IgnorePatterns { patterns })
}
}
#[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
}
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => {
#[test]
fn $name() {
use options::parser::{Args, Arg};
use std::ffi::OsString;
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);
}
};
}
mod sort_fields {
use super::*;
// Default behaviour
test!(empty: SortField <- [] => 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)));
// Errors
test!(error: SortField <- ["--sort=colour"] => 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)));
}
mod dot_filters {
use super::*;
// Default behaviour
test!(empty: DotFilter <- [] => 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));
// --all and --tree
test!(tree_a: DotFilter <- ["-Ta"] => Ok(DotFilter::Dotfiles));
test!(tree_aa: DotFilter <- ["-Taa"] => Err(Misfire::TreeAllAll));
}
mod ignore_patternses {
use super::*;
use glob;
fn pat(string: &'static str) -> glob::Pattern {
glob::Pattern::new(string).unwrap()
}
// Various numbers of globs
test!(none: IgnorePatterns <- [] => Ok(IgnorePatterns { patterns: vec![] }));
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"] => Ok(IgnorePatterns { patterns: vec![ pat("*.ogg") ] }));
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"] => Ok(IgnorePatterns { patterns: vec![ pat("*.ogg"), pat("*.MP3") ] }));
test!(loads: IgnorePatterns <- ["-I*|?|.|*"] => Ok(IgnorePatterns { patterns: vec![ pat("*"), pat("?"), pat("."), pat("*") ] }));
}
}

64
src/options/flags.rs Normal file
View File

@ -0,0 +1,64 @@
use options::parser::{Arg, Args, TakesValue};
// exa options
pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value: TakesValue::Forbidden };
pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden };
// display options
pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden };
pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden };
pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden };
pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden };
pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden };
pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden };
pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden };
pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary };
pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary };
pub static COLOR_SCALE: Arg = Arg { short: None, long: "color-scale", takes_value: TakesValue::Forbidden };
pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden };
// filtering and sorting options
pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden };
pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden };
pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary };
pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden };
pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary };
pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary };
pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden };
// display options
pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden };
pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden };
pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden };
pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden };
pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden };
pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden };
pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden };
pub static BLOCKS: Arg = Arg { short: Some(b'S'), long: "blocks", takes_value: TakesValue::Forbidden };
pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary };
pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden };
pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden };
pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary };
// optional feature options
pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden };
pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden };
pub static ALL_ARGS: Args = Args(&[
&VERSION, &HELP,
&ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY,
&COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE,
&ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &IGNORE_GLOB, &DIRS_FIRST,
&BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &BLOCKS,
&TIME, &ACCESSED, &CREATED, &TIME_STYLE,
&GIT, &EXTENDED,
]);

View File

@ -1,5 +1,9 @@
use std::fmt;
use options::flags;
use options::parser::MatchedFlags;
use fs::feature::xattr;
static OPTIONS: &str = r##"
-?, --help show list of command-line options
@ -46,14 +50,45 @@ LONG VIEW OPTIONS
static GIT_HELP: &str = r##" --git list each file's Git status, if tracked"##;
static EXTENDED_HELP: &str = r##" -@, --extended list each file's extended attributes and sizes"##;
/// All the information needed to display the help text, which depends
/// on which features are enabled and whether the user only wants to
/// see one sections help.
#[derive(PartialEq, Debug)]
pub struct HelpString {
pub only_long: bool,
pub git: bool,
pub xattrs: bool,
/// Only show the help for the long section, not all the help.
only_long: bool,
/// Whether the --git option should be included in the help.
git: bool,
/// Whether the --extended option should be included in the help.
xattrs: bool,
}
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.
pub fn deduce(matches: &MatchedFlags) -> Result<(), HelpString> {
if matches.has(&flags::HELP) {
let only_long = matches.has(&flags::LONG);
let git = cfg!(feature="git");
let xattrs = xattr::ENABLED;
Err(HelpString { only_long, git, xattrs })
}
else {
Ok(()) // no help needs to be shown
}
}
}
impl fmt::Display for HelpString {
/// Format this help options into an actual string of help
/// text to be displayed to the user.
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
try!(write!(f, "Usage:\n exa [options] [files...]\n"));
@ -74,3 +109,38 @@ impl fmt::Display for HelpString {
Ok(())
}
}
#[cfg(test)]
mod test {
use options::Options;
use std::ffi::OsString;
fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
#[test]
fn help() {
let args = [ os("--help") ];
let opts = Options::getopts(&args);
assert!(opts.is_err())
}
#[test]
fn help_with_file() {
let args = [ os("--help"), os("me") ];
let opts = Options::getopts(&args);
assert!(opts.is_err())
}
#[test]
fn unhelpful() {
let args = [];
let opts = Options::getopts(&args);
assert!(opts.is_ok()) // no help when --help isnt passed
}
}

View File

@ -1,10 +1,11 @@
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::num::ParseIntError;
use getopts;
use glob;
use options::help::HelpString;
use options::{HelpString, VersionString};
use options::parser::{Arg, ParseError};
/// A list of legal choices for an argument-taking option
@ -13,7 +14,7 @@ pub struct Choices(&'static [&'static str]);
impl fmt::Display for Choices {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "(choices: {})", self.0.join(" "))
write!(f, "(choices: {})", self.0.join(", "))
}
}
@ -22,29 +23,32 @@ impl fmt::Display for Choices {
#[derive(PartialEq, Debug)]
pub enum Misfire {
/// The getopts crate didnt like these arguments.
InvalidOptions(getopts::Fail),
/// The getopts crate didnt like these Arguments.
InvalidOptions(ParseError),
/// The user supplied an illegal choice to an argument
BadArgument(getopts::Fail, Choices),
/// The user supplied an illegal choice to an Argument.
BadArgument(&'static Arg, OsString, Choices),
/// The user asked for help. This isnt strictly an error, which is why
/// this enum isnt named Error!
Help(HelpString),
/// The user wanted the version number.
Version,
Version(VersionString),
/// Two options were given that conflict with one another.
Conflict(&'static str, &'static str),
Conflict(&'static Arg, &'static Arg),
/// An option was given that does nothing when another one either is or
/// isn't present.
Useless(&'static str, bool, &'static str),
Useless(&'static Arg, bool, &'static Arg),
/// An option was given that does nothing when either of two other options
/// are not present.
Useless2(&'static str, &'static str, &'static str),
Useless2(&'static Arg, &'static Arg, &'static Arg),
/// A very specific edge case where --tree cant be used with --all twice.
TreeAllAll,
/// A numeric option was given that failed to be parsed as a number.
FailedParse(ParseIntError),
@ -58,9 +62,9 @@ impl Misfire {
/// The OS return code this misfire should signify.
pub fn is_error(&self) -> bool {
match *self {
Misfire::Help(_) => false,
Misfire::Version => false,
_ => true,
Misfire::Help(_) => false,
Misfire::Version(_) => false,
_ => true,
}
}
@ -68,10 +72,8 @@ impl Misfire {
/// argument. This has to use one of the `getopts` failure
/// variants--its meant to take just an option name, rather than an
/// option *and* an argument, but it works just as well.
pub fn bad_argument(option: &str, otherwise: &str, legal: &'static [&'static str]) -> Misfire {
Misfire::BadArgument(getopts::Fail::UnrecognizedOption(format!(
"--{} {}",
option, otherwise)), Choices(legal))
pub fn bad_argument(option: &'static Arg, otherwise: &OsStr, legal: &'static [&'static str]) -> Misfire {
Misfire::BadArgument(option, otherwise.to_os_string(), Choices(legal))
}
}
@ -86,16 +88,17 @@ impl fmt::Display for Misfire {
use self::Misfire::*;
match *self {
InvalidOptions(ref e) => write!(f, "{}", e),
BadArgument(ref e, ref c) => write!(f, "{} {}", e, c),
Help(ref text) => write!(f, "{}", text),
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b),
Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e),
BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no value {:?} (Choices: {})", a, b, c),
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),
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),
TreeAllAll => write!(f, "Option --tree is useless given --all --all."),
FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e),
}
}
}

View File

@ -1,24 +1,97 @@
use std::ffi::OsStr;
//! Parsing command-line strings into exa options.
//!
//! This module imports exas configuration types, such as `View` (the details
//! of displaying multiple files) and `DirAction` (what to do when encountering
//! a directory), and implements `deduce` methods on them so they can be
//! configured using command-line options.
//!
//!
//! ## Useless and overridden options
//!
//! Lets say exa was invoked with just one argument: `exa --inode`. The
//! `--inode` option is used in the details view, where it adds the inode
//! column to the output. But because the details view is *only* activated with
//! the `--long` argument, adding `--inode` without it would not have any
//! effect.
//!
//! For a long time, exas philosophy was that the user should be warned
//! whenever they could be mistaken like this. If you tell exa to display the
//! inode, and it *doesnt* display the inode, isnt that more annoying than
//! having it throw an error back at you?
//!
//! However, this doesnt take into account *configuration*. Say a user wants
//! to configure exa so that it lists inodes in the details view, but otherwise
//! functions normally. A common way to do this for command-line programs is to
//! define a shell alias that specifies the details they want to use every
//! time. For the inode column, the alias would be:
//!
//! `alias exa="exa --inode"`
//!
//! Using this alias means that although the inode column will be shown in the
//! details view, youre now *only* allowed to use the details view, as any
//! other view type will result in an error. Oops!
//!
//! Another example is when an option is specified twice, such as `exa
//! --sort=Name --sort=size`. Did the user change their mind about sorting, and
//! accidentally specify the option twice?
//!
//! Again, exa rejected this case, throwing an error back to the user instead
//! of trying to guess how they want their output sorted. And again, this
//! doesnt take into account aliases being used to set defaults. A user who
//! wants their files to be sorted case-insensitively may configure their shell
//! with the following:
//!
//! `alias exa="exa --sort=Name"`
//!
//! Just like the earlier example, the user now cant use any other sort order,
//! because exa refuses to guess which one they meant. Its *more* annoying to
//! have to go back and edit the command than if there were no error.
//!
//! Fortunately, theres a heuristic for telling which options came from an
//! alias and which came from the actual command-line: aliased options are
//! nearer the beginning of the options array, and command-line options are
//! nearer the end. This means that after the options have been parsed, exa
//! needs to traverse them *backwards* to find the last-most-specified one.
//!
//! For example, invoking exa with `exa --sort=size` when that alias is present
//! would result in a full command-line of:
//!
//! `exa --sort=Name --sort=size`
//!
//! `--sort=size` should override `--sort=Name` because its closer to the end
//! of the arguments array. In fact, because theres no way to tell where the
//! arguments came from -- its just a heuristic -- this will still work even
//! if no aliases are being used!
//!
//! Finally, this isnt just useful when options could override each other.
//! Creating an alias `exal=”exa --long --inode --header”` then invoking `exal
//! --grid --long` shouldnt complain about `--long` being given twice when
//! its clear what the user wants.
use getopts;
use fs::feature::xattr;
use std::ffi::{OsStr, OsString};
use fs::dir_action::DirAction;
use fs::filter::FileFilter;
use output::{View, Mode};
use output::details;
mod dir_action;
pub use self::dir_action::{DirAction, RecurseOptions};
mod filter;
pub use self::filter::{FileFilter, SortField, SortCase};
mod view;
mod help;
use self::help::HelpString;
mod version;
use self::version::VersionString;
mod misfire;
pub use self::misfire::Misfire;
mod view;
pub use self::view::{View, Mode};
mod parser;
mod flags;
use self::parser::MatchedFlags;
/// These **options** represent a parsed, error-checked versions of the
@ -39,85 +112,22 @@ pub struct Options {
impl Options {
// Even though the arguments go in as OsStrings, they come out
// as Strings. Invalid UTF-8 wont be parsed, but it wont make
// exa core dump either.
//
// https://github.com/rust-lang-nursery/getopts/pull/29
/// Call getopts on the given slice of command-line strings.
#[allow(unused_results)]
pub fn getopts<C>(args: C) -> Result<(Options, Vec<String>), Misfire>
where C: IntoIterator, C::Item: AsRef<OsStr> {
let mut opts = getopts::Options::new();
pub fn getopts<'args, I>(args: I) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString> {
use options::parser::Matches;
opts.optflag("v", "version", "show version of exa");
opts.optflag("?", "help", "show list of command-line options");
// Display options
opts.optflag("1", "oneline", "display one entry per line");
opts.optflag("l", "long", "display extended file metadata in a table");
opts.optflag("G", "grid", "display entries as a grid (default)");
opts.optflag("x", "across", "sort the grid across, rather than downwards");
opts.optflag("R", "recurse", "recurse into directories");
opts.optflag("T", "tree", "recurse into directories as a tree");
opts.optflag("F", "classify", "display type indicator by file names (one of */=@|)");
opts.optopt ("", "color", "when to use terminal colours", "WHEN");
opts.optopt ("", "colour", "when to use terminal colours", "WHEN");
opts.optflag("", "color-scale", "highlight levels of file sizes distinctly");
opts.optflag("", "colour-scale", "highlight levels of file sizes distinctly");
// Filtering and sorting options
opts.optflag("", "group-directories-first", "sort directories before other files");
opts.optflagmulti("a", "all", "show hidden and 'dot' files");
opts.optflag("d", "list-dirs", "list directories like regular files");
opts.optopt ("L", "level", "limit the depth of recursion", "DEPTH");
opts.optflag("r", "reverse", "reverse the sert order");
opts.optopt ("s", "sort", "which field to sort by", "WORD");
opts.optopt ("I", "ignore-glob", "ignore files that match these glob patterns", "GLOB1|GLOB2...");
// Long view options
opts.optflag("b", "binary", "list file sizes with binary prefixes");
opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
opts.optflag("g", "group", "list each file's group");
opts.optflag("h", "header", "add a header row to each column");
opts.optflag("H", "links", "list each file's number of hard links");
opts.optflag("i", "inode", "list each file's inode number");
opts.optflag("m", "modified", "use the modified timestamp field");
opts.optflag("S", "blocks", "list each file's number of file system blocks");
opts.optopt ("t", "time", "which timestamp field to show", "WORD");
opts.optflag("u", "accessed", "use the accessed timestamp field");
opts.optflag("U", "created", "use the created timestamp field");
opts.optopt ("", "time-style", "how to format timestamp fields", "STYLE");
if cfg!(feature="git") {
opts.optflag("", "git", "list each file's git status");
}
if xattr::ENABLED {
opts.optflag("@", "extended", "list each file's extended attribute keys and sizes");
}
let matches = match opts.parse(args) {
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args) {
Ok(m) => m,
Err(e) => return Err(Misfire::InvalidOptions(e)),
};
if matches.opt_present("help") {
let help = HelpString {
only_long: matches.opt_present("long"),
git: cfg!(feature="git"),
xattrs: xattr::ENABLED,
};
HelpString::deduce(&flags).map_err(Misfire::Help)?;
VersionString::deduce(&flags).map_err(Misfire::Version)?;
return Err(Misfire::Help(help));
}
else if matches.opt_present("version") {
return Err(Misfire::Version);
}
let options = Options::deduce(&matches)?;
Ok((options, matches.free))
let options = Options::deduce(&flags)?;
Ok((options, frees))
}
/// Whether the View specified in this set of options includes a Git
@ -133,7 +143,7 @@ impl Options {
/// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed.
fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Options, Misfire> {
let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches)?;
@ -143,165 +153,134 @@ impl Options {
}
#[cfg(test)]
mod test {
use super::{Options, Misfire, SortField, SortCase};
use fs::DotFilter;
use super::{Options, Misfire, flags};
use std::ffi::OsString;
use fs::filter::{SortField, SortCase};
use fs::feature::xattr;
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
match misfire {
Err(Misfire::Help(_)) => true,
_ => false,
}
}
#[test]
fn help() {
let opts = Options::getopts(&[ "--help".to_string() ]);
assert!(is_helpful(opts))
}
#[test]
fn help_with_file() {
let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
assert!(is_helpful(opts))
/// Creates an `OSStr` (used in tests)
#[cfg(test)]
fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
#[test]
fn files() {
let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
let args = [ os("this file"), os("that file") ];
let outs = Options::getopts(&args).unwrap().1;
assert_eq!(outs, vec![ &os("this file"), &os("that file") ])
}
#[test]
fn no_args() {
let nothing: Vec<String> = Vec::new();
let args = Options::getopts(&nothing).unwrap().1;
assert!(args.is_empty()); // Listing the `.` directory is done in main.rs
}
#[test]
fn file_sizes() {
let opts = Options::getopts(&[ "--long", "--binary", "--bytes" ]);
assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
let nothing: Vec<OsString> = Vec::new();
let outs = Options::getopts(&nothing).unwrap().1;
assert!(outs.is_empty()); // Listing the `.` directory is done in main.rs
}
#[test]
fn just_binary() {
let opts = Options::getopts(&[ "--binary" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
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 opts = Options::getopts(&[ "--bytes" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
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 opts = Options::getopts(&[ "--long", "--across" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
let args = [ os("--long"), os("--across") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::LONG))
}
#[test]
fn oneline_across() {
let opts = Options::getopts(&[ "--oneline", "--across" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
let args = [ os("--oneline"), os("--across") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
}
#[test]
fn just_header() {
let opts = Options::getopts(&[ "--header" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
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 opts = Options::getopts(&[ "--group" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
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 opts = Options::getopts(&[ "--inode" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
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 opts = Options::getopts(&[ "--links" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
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 opts = Options::getopts(&[ "--blocks" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
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 opts = Options::getopts(&[ "--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 opts = Options::getopts(&[ "--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 opts = Options::getopts(&[ "--sort=Name" ]);
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 opts = Options::getopts(&[ "--git" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
let args = [ os("--git") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::GIT, false, &flags::LONG))
}
#[test]
fn extended_without_long() {
if xattr::ENABLED {
let opts = Options::getopts(&[ "--extended" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
let args = [ os("--extended") ];
let opts = Options::getopts(&args);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::EXTENDED, false, &flags::LONG))
}
}
#[test]
fn level_without_recurse_or_tree() {
let opts = Options::getopts(&[ "--level", "69105" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
}
#[test]
fn all_all_with_tree() {
let opts = Options::getopts(&[ "--all", "--all", "--tree" ]);
assert_eq!(opts.unwrap_err(), Misfire::Useless("all --all", true, "tree"))
}
#[test]
fn nowt() {
let nothing: Vec<String> = Vec::new();
let dots = Options::getopts(&nothing).unwrap().0.filter.dot_filter;
assert_eq!(dots, DotFilter::JustFiles);
}
#[test]
fn all() {
let dots = Options::getopts(&[ "--all".to_string() ]).unwrap().0.filter.dot_filter;
assert_eq!(dots, DotFilter::Dotfiles);
}
#[test]
fn allall() {
let dots = Options::getopts(&[ "-a".to_string(), "-a".to_string() ]).unwrap().0.filter.dot_filter;
assert_eq!(dots, DotFilter::DotfilesAndDots);
}
}

583
src/options/parser.rs Normal file
View File

@ -0,0 +1,583 @@
//! A general parser for command-line options.
//!
//! exa uses its own hand-rolled parser for command-line options. It supports
//! the following syntax:
//!
//! - Long options: `--inode`, `--grid`
//! - Long options with values: `--sort size`, `--level=4`
//! - Short options: `-i`, `-G`
//! - Short options with values: `-ssize`, `-L=4`
//!
//! These values can be mixed and matched: `exa -lssize --grid`. If youve used
//! other command-line programs, then hopefully itll work much like them.
//!
//! Because exa already has its own files for the help text, shell completions,
//! man page, and readme, so it can get away with having the options parser do
//! very little: all it really needs to do is parse a slice of strings.
//!
//!
//! ## UTF-8 and `OsStr`
//!
//! The parser uses `OsStr` as its string type. This is necessary for exa to
//! list files that have invalid UTF-8 in their names: by treating file paths
//! as bytes with no encoding, a file can be specified on the command-line and
//! be looked up without having to be encoded into a `str` first.
//!
//! It also avoids the overhead of checking for invalid UTF-8 when parsing
//! command-line options, as all the options and their values (such as
//! `--sort size`) are guaranteed to just be 8-bit ASCII.
use std::ffi::{OsStr, OsString};
use std::fmt;
/// A **short argument** is a single ASCII character.
pub type ShortArg = u8;
/// A **long argument** is a string. This can be a UTF-8 string, even though
/// the arguments will all be unchecked OsStrings, because we dont actually
/// store the users input after its been matched to a flag, we just store
/// which flag it was.
pub type LongArg = &'static str;
/// A **flag** is either of the two argument types, because they have to
/// be in the same array together.
#[derive(PartialEq, Debug, Clone)]
pub enum Flag {
Short(ShortArg),
Long(LongArg),
}
impl Flag {
fn matches(&self, arg: &Arg) -> bool {
match *self {
Flag::Short(short) => arg.short == Some(short),
Flag::Long(long) => arg.long == long,
}
}
}
/// Whether redundant arguments should be considered a problem.
#[derive(PartialEq, Debug)]
#[allow(dead_code)] // until strict mode is actually implemented
pub enum Strictness {
/// Throw an error when an argument doesnt do anything, either because
/// it requires another argument to be specified, or because two conflict.
ComplainAboutRedundantArguments,
/// Search the arguments list back-to-front, giving ones specified later
/// in the list priority over earlier ones.
UseLastArguments,
}
/// Whether a flag takes a value. This is applicable to both long and short
/// arguments.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum TakesValue {
/// This flag has to be followed by a value.
Necessary,
/// This flag will throw an error if theres a value after it.
Forbidden,
}
/// An **argument** can be matched by one of the users input strings.
#[derive(PartialEq, Debug)]
pub struct Arg {
/// The short argument that matches it, if any.
pub short: Option<ShortArg>,
/// The long argument that matches it. This is non-optional; all flags
/// should at least have a descriptive long name.
pub long: LongArg,
/// Whether this flag takes a value or not.
pub takes_value: TakesValue,
}
impl fmt::Display for Arg {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "--{}", self.long)?;
if let Some(short) = self.short {
write!(f, " (-{})", short as char)?;
}
Ok(())
}
}
/// Literally just several args.
#[derive(PartialEq, Debug)]
pub struct Args(pub &'static [&'static Arg]);
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>
where I: IntoIterator<Item=&'args OsString> {
use std::os::unix::ffi::OsStrExt;
use self::TakesValue::*;
let mut parsing = true;
// The results that get built up.
let mut result_flags = Vec::new();
let mut frees: Vec<&OsStr> = Vec::new();
// Iterate over the inputs with “while let” because we need to advance
// the iterator manually whenever an argument that takes a value
// doesnt have one in its string so it needs the next one.
let mut inputs = inputs.into_iter();
while let Some(arg) = inputs.next() {
let bytes = arg.as_bytes();
// Stop parsing if one of the arguments is the literal string “--”.
// This allows a file named “--arg” to be specified by passing in
// the pair “-- --arg”, without it getting matched as a flag that
// doesnt exist.
if !parsing {
frees.push(arg)
}
else if arg == "--" {
parsing = false;
}
// If the string starts with *two* dashes then its a long argument.
else if bytes.starts_with(b"--") {
let long_arg_name = OsStr::from_bytes(&bytes[2..]);
// If theres an equals in it, then the string before the
// equals will be the flags name, and the string after it
// will be its value.
if let Some((before, after)) = split_on_equals(long_arg_name) {
let arg = self.lookup_long(before)?;
let flag = Flag::Long(arg.long);
match arg.takes_value {
Necessary => result_flags.push((flag, Some(after))),
Forbidden => return Err(ParseError::ForbiddenValue { flag })
}
}
// If theres no equals, then the entire string (apart from
// the dashes) is the argument name.
else {
let arg = self.lookup_long(long_arg_name)?;
let flag = Flag::Long(arg.long);
match arg.takes_value {
Forbidden => result_flags.push((flag, None)),
Necessary => {
if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
else {
return Err(ParseError::NeedsValue { flag })
}
}
}
}
}
// If the string starts with *one* dash then its one or more
// short arguments.
else if bytes.starts_with(b"-") && arg != "-" {
let short_arg = OsStr::from_bytes(&bytes[1..]);
// If theres an equals in it, then the argument immediately
// before the equals was the one that has the value, with the
// others (if any) as value-less short ones.
//
// -x=abc => x=abc
// -abcdx=fgh => a, b, c, d, x=fgh
// -x= => error
// -abcdx= => error
//
// Theres no way to give two values in a cluster like this:
// it's an error if any of the first set of arguments actually
// takes a value.
if let Some((before, after)) = split_on_equals(short_arg) {
let (arg_with_value, other_args) = before.as_bytes().split_last().unwrap();
// Process the characters immediately following the dash...
for byte in other_args {
let arg = self.lookup_short(*byte)?;
let flag = Flag::Short(*byte);
match arg.takes_value {
Forbidden => result_flags.push((flag, None)),
Necessary => return Err(ParseError::NeedsValue { flag })
}
}
// ...then the last one and the value after the equals.
let arg = self.lookup_short(*arg_with_value)?;
let flag = Flag::Short(arg.short.unwrap());
match arg.takes_value {
Necessary => result_flags.push((flag, Some(after))),
Forbidden => return Err(ParseError::ForbiddenValue { flag })
}
}
// If theres no equals, then every character is parsed as
// its own short argument. However, if any of the arguments
// takes a value, then the *rest* of the string is used as
// its value, and if there's no rest of the string, then it
// uses the next one in the iterator.
//
// -a => a
// -abc => a, b, c
// -abxdef => a, b, x=def
// -abx def => a, b, x=def
// -abx => error
//
else {
for (index, byte) in bytes.into_iter().enumerate().skip(1) {
let arg = self.lookup_short(*byte)?;
let flag = Flag::Short(*byte);
match arg.takes_value {
Forbidden => result_flags.push((flag, None)),
Necessary => {
if index < bytes.len() - 1 {
let remnants = &bytes[index+1 ..];
result_flags.push((flag, Some(OsStr::from_bytes(remnants))));
break;
}
else if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
else {
return Err(ParseError::NeedsValue { flag })
}
}
}
}
}
}
// Otherwise, its a free string, usually a file name.
else {
frees.push(arg)
}
}
Ok(Matches { frees, flags: MatchedFlags { flags: result_flags } })
}
fn lookup_short<'a>(&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> {
match self.0.into_iter().find(|arg| arg.long == long) {
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() })
}
}
}
/// The **matches** are the result of parsing the users command-line strings.
#[derive(PartialEq, Debug)]
pub struct Matches<'args> {
/// The flags that were parsed from the users input.
pub flags: MatchedFlags<'args>,
/// All the strings that werent matched as arguments, as well as anything
/// after the special "--" string.
pub frees: Vec<&'args OsStr>,
}
#[derive(PartialEq, Debug)]
pub struct MatchedFlags<'args> {
/// The individual flags from the users input, in the order they were
/// originally given.
///
/// Long and short arguments need to be kept in the same vector because
/// 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>)>,
}
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()
}
/// 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())
}
// 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.
pub fn count(&self, arg: &Arg) -> usize {
self.flags.iter()
.filter(|tuple| tuple.0.matches(arg))
.count()
}
}
/// A problem with the user's input that meant it couldn't be parsed into a
/// coherent list of arguments.
#[derive(PartialEq, Debug)]
pub enum ParseError {
/// A flag that has to take a value was not given one.
NeedsValue { flag: Flag },
/// A flag that can't take a value *was* given one.
ForbiddenValue { flag: Flag },
/// A short argument, either alone or in a cluster, was not
/// recognised by the program.
UnknownShortArgument { attempt: ShortArg },
/// A long argument was not recognised by the program.
/// We dont have a known &str version of the flag, so
/// this may not be valid UTF-8.
UnknownArgument { attempt: OsString },
}
// Its technically possible for ParseError::UnknownArgument to borrow its
// OsStr rather than owning it, but that would give ParseError a lifetime,
// which would give Misfire a lifetime, which gets used everywhere. And this
// only happens when an error occurs, so its not really worth it.
/// Splits a string on its `=` character, returning the two substrings on
/// either side. Returns `None` if theres no equals or a string is missing.
fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
use std::os::unix::ffi::OsStrExt;
if let Some(index) = input.as_bytes().iter().position(|elem| *elem == b'=') {
let (before, after) = input.as_bytes().split_at(index);
// The after string contains the = that we need to remove.
if before.len() >= 1 && after.len() >= 2 {
return Some((OsStr::from_bytes(before),
OsStr::from_bytes(&after[1..])))
}
}
None
}
/// Creates an `OSString` (used in tests)
#[cfg(test)]
fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
#[cfg(test)]
mod split_test {
use super::{split_on_equals, os};
macro_rules! test_split {
($name:ident: $input:expr => None) => {
#[test]
fn $name() {
assert_eq!(split_on_equals(&os($input)),
None);
}
};
($name:ident: $input:expr => $before:expr, $after:expr) => {
#[test]
fn $name() {
assert_eq!(split_on_equals(&os($input)),
Some((&*os($before), &*os($after))));
}
};
}
test_split!(empty: "" => None);
test_split!(letter: "a" => None);
test_split!(just: "=" => None);
test_split!(intro: "=bbb" => None);
test_split!(denou: "aaa=" => None);
test_split!(equals: "aaa=bbb" => "aaa", "bbb");
test_split!(sort: "--sort=size" => "--sort", "size");
test_split!(more: "this=that=other" => "this", "that=other");
}
#[cfg(test)]
mod parse_test {
use super::*;
macro_rules! test {
($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => {
#[test]
fn $name() {
// Annoyingly the input &strs need to be converted to OsStrings
let inputs: Vec<OsString> = $inputs.as_ref().into_iter().map(|&o| os(o)).collect();
// Same with the frees
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 got = Args(TEST_ARGS).parse(inputs.iter());
let expected = Ok(Matches { frees, flags: MatchedFlags { flags } });
assert_eq!(got, expected);
}
};
($name:ident: $inputs:expr => error $error:expr) => {
#[test]
fn $name() {
use self::ParseError::*;
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let got = Args(TEST_ARGS).parse(bits.iter());
assert_eq!(got, Err($error));
}
};
}
static TEST_ARGS: &[&Arg] = &[
&Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden },
&Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden },
&Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary }
];
// Just filenames
test!(empty: [] => frees: [], flags: []);
test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []);
// Dashes and double dashes
test!(one_dash: ["-"] => frees: [ "-" ], flags: []);
test!(two_dashes: ["--"] => frees: [], flags: []);
test!(two_file: ["--", "file"] => frees: [ "file" ], flags: []);
test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []);
test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []);
// Long args
test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]);
test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]);
test!(long_two: ["--long", "--verbose"] => frees: [], flags: [ (Flag::Long("long"), None), (Flag::Long("verbose"), None) ]);
// 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")) ]);
// Short args
test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]);
test!(short_then: ["-l", "4"] => frees: [ "4" ], flags: [ (Flag::Short(b'l'), None) ]);
test!(short_two: ["-lv"] => frees: [], flags: [ (Flag::Short(b'l'), None), (Flag::Short(b'v'), None) ]);
test!(mixed: ["-v", "--long"] => frees: [], flags: [ (Flag::Short(b'v'), None), (Flag::Long("long"), None) ]);
// 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")) ]);
// Unknown args
test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: os("quiet") });
test!(unknown_long_eq: ["--quiet=shhh"] => error UnknownArgument { attempt: os("quiet") });
test!(unknown_short: ["-q"] => error UnknownShortArgument { attempt: b'q' });
test!(unknown_short_2nd: ["-lq"] => error UnknownShortArgument { attempt: b'q' });
test!(unknown_short_eq: ["-q=shhh"] => error UnknownShortArgument { attempt: b'q' });
test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' });
}
#[cfg(test)]
mod matches_test {
use super::*;
macro_rules! 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);
}
};
}
static VERBOSE: Arg = Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden };
static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary };
test!(short_never: [], has VERBOSE => false);
test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true);
test!(short_twice: [(Flag::Short(b'v'), None), (Flag::Short(b'v'), None)], has VERBOSE => true);
test!(long_once: [(Flag::Long("verbose"), None)], has VERBOSE => true);
test!(long_twice: [(Flag::Long("verbose"), None), (Flag::Long("verbose"), None)], has VERBOSE => true);
test!(long_mixed: [(Flag::Long("verbose"), None), (Flag::Short(b'v'), None)], has VERBOSE => true);
#[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));
}
#[test]
fn rightmost_count() {
let everything = os("everything");
let nothing = os("nothing");
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)),
(Flag::Short(b'c'), Some(&*nothing)) ]
};
assert_eq!(flags.get(&COUNT), Some(&*nothing));
}
#[test]
fn no_count() {
let flags = MatchedFlags { flags: Vec::new() };
assert!(!flags.has(&COUNT));
}
}

58
src/options/version.rs Normal file
View File

@ -0,0 +1,58 @@
use std::fmt;
use options::flags;
use options::parser::MatchedFlags;
/// All the information needed to display the version information.
#[derive(PartialEq, Debug)]
pub struct VersionString {
/// The version number from cargo.
cargo: &'static str,
}
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.
pub fn deduce(matches: &MatchedFlags) -> Result<(), VersionString> {
if matches.has(&flags::VERSION) {
Err(VersionString { cargo: env!("CARGO_PKG_VERSION") })
}
else {
Ok(()) // no version needs to be shown
}
}
}
impl fmt::Display for VersionString {
/// Format this help options into an actual string of help
/// text to be displayed to the user.
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "exa v{}", self.cargo)
}
}
#[cfg(test)]
mod test {
use options::Options;
use std::ffi::OsString;
fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
#[test]
fn help() {
let args = [ os("--version") ];
let opts = Options::getopts(&args);
assert!(opts.is_err())
}
}

View File

@ -1,28 +1,22 @@
use std::env::var_os;
use getopts;
use info::filetype::FileExtensions;
use output::Colours;
use output::{grid, details};
use output::{View, Mode, grid, details};
use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions};
use output::file_name::{Classify, FileStyle};
use output::time::TimeFormat;
use options::Misfire;
use fs::feature::xattr;
/// The **view** contains all information about how to format output.
#[derive(Debug)]
pub struct View {
pub mode: Mode,
pub colours: Colours,
pub style: FileStyle,
}
use options::{flags, Misfire};
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: &getopts::Matches) -> Result<View, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<View, Misfire> {
let mode = Mode::deduce(matches)?;
let colours = Colours::deduce(matches)?;
let style = FileStyle::deduce(matches);
@ -31,52 +25,44 @@ impl View {
}
/// The **mode** is the “type” of output.
#[derive(Debug)]
pub enum Mode {
Grid(grid::Options),
Details(details::Options),
GridDetails(grid::Options, details::Options),
Lines,
}
impl Mode {
/// Determine the mode from the command-line arguments.
pub fn deduce(matches: &getopts::Matches) -> Result<Mode, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<Mode, Misfire> {
use options::misfire::Misfire::*;
let long = || {
if matches.opt_present("across") && !matches.opt_present("grid") {
Err(Useless("across", true, "long"))
if matches.has(&flags::ACROSS) && !matches.has(&flags::GRID) {
Err(Useless(&flags::ACROSS, true, &flags::LONG))
}
else if matches.opt_present("oneline") {
Err(Useless("oneline", true, "long"))
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.opt_present("header"),
xattr: xattr::ENABLED && matches.opt_present("extended"),
header: matches.has(&flags::HEADER),
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED),
})
}
};
let long_options_scan = || {
for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
if matches.opt_present(option) {
return Err(Useless(option, false, "long"));
for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
if matches.has(option) {
return Err(Useless(*option, false, &flags::LONG));
}
}
if cfg!(feature="git") && matches.opt_present("git") {
Err(Useless("git", false, "long"))
if cfg!(feature="git") && matches.has(&flags::GIT) {
Err(Useless(&flags::GIT, false, &flags::LONG))
}
else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
Err(Useless2("level", "recurse", "tree"))
else if matches.has(&flags::LEVEL) && !matches.has(&flags::RECURSE) && !matches.has(&flags::TREE) {
Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))
}
else if xattr::ENABLED && matches.opt_present("extended") {
Err(Useless("extended", false, "long"))
else if xattr::ENABLED && matches.has(&flags::EXTENDED) {
Err(Useless(&flags::EXTENDED, false, &flags::LONG))
}
else {
Ok(())
@ -85,15 +71,15 @@ impl Mode {
let other_options_scan = || {
if let Some(width) = TerminalWidth::deduce()?.width() {
if matches.opt_present("oneline") {
if matches.opt_present("across") {
Err(Useless("across", true, "oneline"))
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.opt_present("tree") {
else if matches.has(&flags::TREE) {
let details = details::Options {
table: None,
header: false,
@ -104,7 +90,7 @@ impl Mode {
}
else {
let grid = grid::Options {
across: matches.opt_present("across"),
across: matches.has(&flags::ACROSS),
console_width: width,
};
@ -116,7 +102,7 @@ impl Mode {
// as the programs stdout being connected to a file, then
// fallback to the lines view.
if matches.opt_present("tree") {
if matches.has(&flags::TREE) {
let details = details::Options {
table: None,
header: false,
@ -131,9 +117,9 @@ impl Mode {
}
};
if matches.opt_present("long") {
if matches.has(&flags::LONG) {
let details = long()?;
if matches.opt_present("grid") {
if matches.has(&flags::GRID) {
match other_options_scan()? {
Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)),
others => return Ok(others),
@ -196,17 +182,17 @@ impl TerminalWidth {
impl TableOptions {
fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire> {
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.opt_present("inode"),
links: matches.opt_present("links"),
blocks: matches.opt_present("blocks"),
group: matches.opt_present("group"),
git: cfg!(feature="git") && matches.opt_present("git"),
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),
})
}
}
@ -222,12 +208,12 @@ impl SizeFormat {
/// strings of digits in your head. Changing the format to anything else
/// involves the `--binary` or `--bytes` flags, and these conflict with
/// each other.
fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
let binary = matches.opt_present("binary");
let bytes = matches.opt_present("bytes");
fn deduce(matches: &MatchedFlags) -> Result<SizeFormat, Misfire> {
let binary = matches.has(&flags::BINARY);
let bytes = matches.has(&flags::BYTES);
match (binary, bytes) {
(true, true ) => Err(Misfire::Conflict("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),
@ -239,26 +225,36 @@ impl SizeFormat {
impl TimeFormat {
/// Determine how time should be formatted in timestamp columns.
fn deduce(matches: &getopts::Matches) -> Result<TimeFormat, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<TimeFormat, Misfire> {
pub use output::time::{DefaultFormat, ISOFormat};
const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
if let Some(word) = matches.opt_str("time-style") {
match &*word {
"default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
"iso" => Ok(TimeFormat::ISOFormat(ISOFormat::new())),
"long-iso" => Ok(TimeFormat::LongISO),
"full-iso" => Ok(TimeFormat::FullISO),
otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)),
}
let word = match matches.get(&flags::TIME_STYLE) {
Some(w) => w,
None => return Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
};
if word == "default" {
Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
}
else if word == "iso" {
Ok(TimeFormat::ISOFormat(ISOFormat::new()))
}
else if word == "long-iso" {
Ok(TimeFormat::LongISO)
}
else if word == "full-iso" {
Ok(TimeFormat::FullISO)
}
else {
Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
Err(Misfire::bad_argument(&flags::TIME_STYLE, word, STYLES))
}
}
}
static TIMES: &[&str] = &["modified", "accessed", "created"];
impl TimeTypes {
/// Determine which of a files time fields should be displayed for it
@ -271,29 +267,33 @@ impl TimeTypes {
/// Its valid to show more than one column by passing in more than one
/// option, but passing *no* options means that the user just wants to
/// see the default set.
fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
let possible_word = matches.opt_str("time");
let modified = matches.opt_present("modified");
let created = matches.opt_present("created");
let accessed = matches.opt_present("accessed");
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);
if let Some(word) = possible_word {
if modified {
return Err(Misfire::Useless("modified", true, "time"));
Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME))
}
else if created {
return Err(Misfire::Useless("created", true, "time"));
Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME))
}
else if accessed {
return Err(Misfire::Useless("accessed", true, "time"));
Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME))
}
static TIMES: &[& str] = &["modified", "accessed", "created"];
match &*word {
"mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
"acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
"cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
otherwise => Err(Misfire::bad_argument("time", otherwise, TIMES))
else if word == "mod" || word == "modified" {
Ok(TimeTypes { accessed: false, modified: true, created: false })
}
else if word == "acc" || word == "accessed" {
Ok(TimeTypes { accessed: true, modified: false, created: false })
}
else if word == "cr" || word == "created" {
Ok(TimeTypes { accessed: false, modified: false, created: true })
}
else {
Err(Misfire::bad_argument(&flags::TIME, word, TIMES))
}
}
else if modified || created || accessed {
@ -335,31 +335,37 @@ impl Default for TerminalColours {
impl TerminalColours {
/// Determine which terminal colour conditions to use.
fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
const COLOURS: &[&str] = &["always", "auto", "never"];
if let Some(word) = matches.opt_str("color").or_else(|| matches.opt_str("colour")) {
match &*word {
"always" => Ok(TerminalColours::Always),
"auto" | "automatic" => Ok(TerminalColours::Automatic),
"never" => Ok(TerminalColours::Never),
otherwise => Err(Misfire::bad_argument("color", otherwise, COLOURS))
}
let word = match matches.get(&flags::COLOR).or_else(|| matches.get(&flags::COLOUR)) {
Some(w) => w,
None => return Ok(TerminalColours::default()),
};
if word == "always" {
Ok(TerminalColours::Always)
}
else if word == "auto" || word == "automatic" {
Ok(TerminalColours::Automatic)
}
else if word == "never" {
Ok(TerminalColours::Never)
}
else {
Ok(TerminalColours::default())
Err(Misfire::bad_argument(&flags::COLOR, word, COLOURS))
}
}
}
impl Colours {
fn deduce(matches: &getopts::Matches) -> Result<Colours, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Colours, Misfire> {
use self::TerminalColours::*;
let tc = TerminalColours::deduce(matches)?;
if tc == Always || (tc == Automatic && TERM_WIDTH.is_some()) {
let scale = matches.opt_present("color-scale") || matches.opt_present("colour-scale");
let scale = matches.has(&flags::COLOR_SCALE) || matches.has(&flags::COLOUR_SCALE);
Ok(Colours::colourful(scale))
}
else {
@ -371,18 +377,17 @@ impl Colours {
impl FileStyle {
fn deduce(matches: &getopts::Matches) -> FileStyle {
fn deduce(matches: &MatchedFlags) -> FileStyle {
let classify = Classify::deduce(matches);
let exts = FileExtensions;
FileStyle { classify, exts }
}
}
impl Classify {
fn deduce(matches: &getopts::Matches) -> Classify {
if matches.opt_present("classify") { Classify::AddFileIndicators }
else { Classify::JustFilenames }
fn deduce(matches: &MatchedFlags) -> Classify {
if matches.has(&flags::CLASSIFY) { Classify::AddFileIndicators }
else { Classify::JustFilenames }
}
}
@ -399,3 +404,77 @@ lazy_static! {
dimensions_stdout().map(|t| t.0)
};
}
#[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
}
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr => $result:expr) => {
#[test]
fn $name() {
use options::parser::{Args, Arg};
use std::ffi::OsString;
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES,
&flags::TIME, &flags::MODIFIED, &flags::CREATED, &flags::ACCESSED ];
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);
}
};
}
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)));
}
mod time_types {
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!(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 }));
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 }));
// 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 }));
// 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)));
}
}

View File

@ -65,8 +65,9 @@ use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;
use fs::{Dir, File};
use fs::dir_action::RecurseOptions;
use fs::filter::FileFilter;
use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions};
use output::colours::Colours;
use output::cell::TextCell;
use output::tree::{TreeTrunk, TreeParams, TreeDepth};

View File

@ -5,8 +5,8 @@ use term_grid as grid;
use fs::{Dir, File};
use fs::feature::xattr::FileAttributes;
use fs::filter::FileFilter;
use options::FileFilter;
use output::cell::TextCell;
use output::colours::Colours;
use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};

View File

@ -1,3 +1,5 @@
use output::file_name::FileStyle;
pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
pub use self::colours::Colours;
pub use self::escape::escape;
@ -15,3 +17,22 @@ mod colours;
mod escape;
mod render;
mod tree;
/// The **view** contains all information about how to format output.
#[derive(Debug)]
pub struct View {
pub mode: Mode,
pub colours: Colours,
pub style: FileStyle,
}
/// The **mode** is the “type” of output.
#[derive(Debug)]
pub enum Mode {
Grid(grid::Options),
Details(details::Options),
GridDetails(grid::Options, details::Options),
Lines,
}

View File

@ -78,7 +78,7 @@ COLUMNS=80 $exa $testcases/file-names -R 2>&1 | diff -q - $results/file_names_R
$exa $testcases/file-names -T 2>&1 | diff -q - $results/file_names_T || exit 1
# At least make sure it handles invalid UTF-8 arguments without crashing
$exa $testcases/file-names/* 2>/dev/null
$exa $testcases/file-names/* >/dev/null || exit 1
# Sorting and extension file types