mirror of
https://github.com/Llewellynvdm/exa.git
synced 2025-02-14 08:10:25 +00:00
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:
commit
b5bcf22612
@ -16,6 +16,7 @@ license = "MIT"
|
||||
[[bin]]
|
||||
name = "exa"
|
||||
path = "src/bin/main.rs"
|
||||
doc = false
|
||||
|
||||
[lib]
|
||||
name = "exa"
|
||||
|
4
Vagrantfile
vendored
4
Vagrantfile
vendored
@ -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
|
||||
|
||||
|
@ -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),
|
||||
|
27
src/exa.rs
27
src/exa.rs
@ -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 isn’t 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
64
src/fs/dir_action.rs
Normal 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
221
src/fs/filter.rs
Normal 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 user’s 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 it’s read
|
||||
/// slows the whole operation down, so many systems will only update the
|
||||
/// timestamp in certain circumstances. This has become common enough that
|
||||
/// it’s 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))
|
||||
}
|
||||
}
|
@ -6,3 +6,5 @@ pub use self::file::{File, FileTarget};
|
||||
|
||||
pub mod feature;
|
||||
pub mod fields;
|
||||
pub mod filter;
|
||||
pub mod dir_action;
|
||||
|
@ -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 wouldn’t 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)));
|
||||
}
|
||||
|
@ -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 user’s 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 user’s
|
||||
/// 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 it’s read
|
||||
/// slows the whole operation down, so many systems will only update the
|
||||
/// timestamp in certain circumstances. This has become common enough that
|
||||
/// it’s 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 user’s
|
||||
/// 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
64
src/options/flags.rs
Normal 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,
|
||||
]);
|
||||
|
@ -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 section’s 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 user’s
|
||||
/// 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 isn’t passed
|
||||
}
|
||||
}
|
||||
|
@ -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 didn’t like these arguments.
|
||||
InvalidOptions(getopts::Fail),
|
||||
/// The getopts crate didn’t 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 isn’t strictly an error, which is why
|
||||
/// this enum isn’t 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 can’t 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--it’s 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,97 @@
|
||||
use std::ffi::OsStr;
|
||||
//! Parsing command-line strings into exa options.
|
||||
//!
|
||||
//! This module imports exa’s 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
|
||||
//!
|
||||
//! Let’s 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, exa’s 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 *doesn’t* display the inode, isn’t that more annoying than
|
||||
//! having it throw an error back at you?
|
||||
//!
|
||||
//! However, this doesn’t 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, you’re 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
|
||||
//! doesn’t 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 can’t use any other sort order,
|
||||
//! because exa refuses to guess which one they meant. It’s *more* annoying to
|
||||
//! have to go back and edit the command than if there were no error.
|
||||
//!
|
||||
//! Fortunately, there’s 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 it’s closer to the end
|
||||
//! of the arguments array. In fact, because there’s no way to tell where the
|
||||
//! arguments came from -- it’s just a heuristic -- this will still work even
|
||||
//! if no aliases are being used!
|
||||
//!
|
||||
//! Finally, this isn’t just useful when options could override each other.
|
||||
//! Creating an alias `exal=”exa --long --inode --header”` then invoking `exal
|
||||
//! --grid --long` shouldn’t complain about `--long` being given twice when
|
||||
//! it’s 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 won’t be parsed, but it won’t 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 they’ve 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(¬hing).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(¬hing).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(¬hing).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
583
src/options/parser.rs
Normal 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 you’ve used
|
||||
//! other command-line programs, then hopefully it’ll 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 don’t actually
|
||||
/// store the user’s input after it’s 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 doesn’t 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 there’s a value after it.
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
|
||||
/// An **argument** can be matched by one of the user’s 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
|
||||
// doesn’t 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
|
||||
// doesn’t exist.
|
||||
if !parsing {
|
||||
frees.push(arg)
|
||||
}
|
||||
else if arg == "--" {
|
||||
parsing = false;
|
||||
}
|
||||
|
||||
// If the string starts with *two* dashes then it’s a long argument.
|
||||
else if bytes.starts_with(b"--") {
|
||||
let long_arg_name = OsStr::from_bytes(&bytes[2..]);
|
||||
|
||||
// If there’s an equals in it, then the string before the
|
||||
// equals will be the flag’s 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 there’s 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 it’s one or more
|
||||
// short arguments.
|
||||
else if bytes.starts_with(b"-") && arg != "-" {
|
||||
let short_arg = OsStr::from_bytes(&bytes[1..]);
|
||||
|
||||
// If there’s 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
|
||||
//
|
||||
// There’s 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 there’s 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, it’s 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 user’s command-line strings.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Matches<'args> {
|
||||
|
||||
/// The flags that were parsed from the user’s input.
|
||||
pub flags: MatchedFlags<'args>,
|
||||
|
||||
/// All the strings that weren’t 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 user’s 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())
|
||||
}
|
||||
|
||||
// It’s annoying that ‘has’ and ‘get’ won’t work when accidentally given
|
||||
// flags that do/don’t 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 don’t have a known &str version of the flag, so
|
||||
/// this may not be valid UTF-8.
|
||||
UnknownArgument { attempt: OsString },
|
||||
}
|
||||
|
||||
// It’s 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 it’s not really worth it.
|
||||
|
||||
|
||||
/// Splits a string on its `=` character, returning the two substrings on
|
||||
/// either side. Returns `None` if there’s 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
58
src/options/version.rs
Normal 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 user’s
|
||||
/// 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())
|
||||
}
|
||||
}
|
@ -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 view’s 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 program’s 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 file’s time fields should be displayed for it
|
||||
@ -271,29 +267,33 @@ impl TimeTypes {
|
||||
/// It’s 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)));
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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};
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user