2015-06-08 20:33:39 +00:00
|
|
|
use std::cmp;
|
|
|
|
use std::default;
|
2015-01-23 19:27:06 +00:00
|
|
|
use std::fmt;
|
2015-02-24 16:05:25 +00:00
|
|
|
use std::num::ParseIntError;
|
2015-05-03 15:25:53 +00:00
|
|
|
use std::os::unix::fs::MetadataExt;
|
2014-05-24 01:17:43 +00:00
|
|
|
|
2015-01-31 16:10:40 +00:00
|
|
|
use getopts;
|
|
|
|
use natord;
|
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
use colours::Colours;
|
|
|
|
use column::Column;
|
|
|
|
use column::Column::*;
|
|
|
|
use dir::Dir;
|
2015-08-25 17:29:23 +00:00
|
|
|
use feature::xattr;
|
2015-06-08 20:33:39 +00:00
|
|
|
use file::File;
|
2015-06-28 12:21:21 +00:00
|
|
|
use output::{Grid, Details, GridDetails, Lines};
|
2015-06-08 20:33:39 +00:00
|
|
|
use term::dimensions;
|
|
|
|
|
2015-01-12 21:14:27 +00:00
|
|
|
|
2015-01-24 12:38:05 +00:00
|
|
|
/// The *Options* struct represents a parsed version of the user's
|
|
|
|
/// command-line options.
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2014-07-06 16:33:40 +00:00
|
|
|
pub struct Options {
|
2015-01-31 16:10:40 +00:00
|
|
|
pub dir_action: DirAction,
|
2015-02-03 17:03:58 +00:00
|
|
|
pub filter: FileFilter,
|
2015-02-05 14:39:56 +00:00
|
|
|
pub view: View,
|
2015-02-03 17:03:58 +00:00
|
|
|
}
|
|
|
|
|
2014-05-24 01:17:43 +00:00
|
|
|
impl Options {
|
2015-01-24 12:38:05 +00:00
|
|
|
|
|
|
|
/// Call getopts on the given slice of command-line strings.
|
2015-02-05 14:39:56 +00:00
|
|
|
pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
|
2015-02-04 14:51:55 +00:00
|
|
|
let mut opts = getopts::Options::new();
|
|
|
|
opts.optflag("1", "oneline", "display one entry per line");
|
|
|
|
opts.optflag("a", "all", "show dot-files");
|
|
|
|
opts.optflag("b", "binary", "use binary prefixes in file sizes");
|
|
|
|
opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
|
|
|
|
opts.optflag("d", "list-dirs", "list directories as regular files");
|
|
|
|
opts.optflag("g", "group", "show group as well as user");
|
2015-06-28 12:21:21 +00:00
|
|
|
opts.optflag("G", "grid", "display entries in a grid view (default)");
|
2015-02-26 14:05:26 +00:00
|
|
|
opts.optflag("", "group-directories-first", "list directories before other files");
|
2015-02-04 14:51:55 +00:00
|
|
|
opts.optflag("h", "header", "show a header row at the top");
|
|
|
|
opts.optflag("H", "links", "show number of hard links");
|
|
|
|
opts.optflag("i", "inode", "show each file's inode number");
|
2015-02-09 16:49:31 +00:00
|
|
|
opts.optflag("l", "long", "display extended details and attributes");
|
2015-02-24 16:05:25 +00:00
|
|
|
opts.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
|
2015-02-09 17:20:42 +00:00
|
|
|
opts.optflag("m", "modified", "display timestamp of most recent modification");
|
2015-02-04 14:51:55 +00:00
|
|
|
opts.optflag("r", "reverse", "reverse order of files");
|
|
|
|
opts.optflag("R", "recurse", "recurse into directories");
|
|
|
|
opts.optopt ("s", "sort", "field to sort by", "WORD");
|
|
|
|
opts.optflag("S", "blocks", "show number of file system blocks");
|
2015-02-09 16:36:51 +00:00
|
|
|
opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
|
2015-02-04 14:51:55 +00:00
|
|
|
opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
|
2015-02-09 17:20:42 +00:00
|
|
|
opts.optflag("u", "accessed", "display timestamp of last access for a file");
|
|
|
|
opts.optflag("U", "created", "display timestamp of creation for a file");
|
2015-02-04 14:51:55 +00:00
|
|
|
opts.optflag("x", "across", "sort multi-column view entries across");
|
2015-03-02 14:54:38 +00:00
|
|
|
|
|
|
|
opts.optflag("", "version", "display version of exa");
|
2015-02-04 14:51:55 +00:00
|
|
|
opts.optflag("?", "help", "show list of command-line options");
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2015-03-10 17:36:29 +00:00
|
|
|
if cfg!(feature="git") {
|
|
|
|
opts.optflag("", "git", "show git status");
|
|
|
|
}
|
|
|
|
|
2015-08-25 17:29:23 +00:00
|
|
|
if xattr::ENABLED {
|
2015-02-24 16:05:25 +00:00
|
|
|
opts.optflag("@", "extended", "display extended attribute keys and sizes in long (-l) output");
|
|
|
|
}
|
|
|
|
|
2015-02-04 14:51:55 +00:00
|
|
|
let matches = match opts.parse(args) {
|
2015-05-16 20:02:28 +00:00
|
|
|
Ok(m) => m,
|
|
|
|
Err(e) => return Err(Misfire::InvalidOptions(e)),
|
2014-11-25 15:54:42 +00:00
|
|
|
};
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2014-11-25 15:54:42 +00:00
|
|
|
if matches.opt_present("help") {
|
2015-02-04 14:51:55 +00:00
|
|
|
return Err(Misfire::Help(opts.usage("Usage:\n exa [options] [files...]")));
|
2014-05-25 16:14:50 +00:00
|
|
|
}
|
2015-03-02 14:54:38 +00:00
|
|
|
else if matches.opt_present("version") {
|
|
|
|
return Err(Misfire::Version);
|
|
|
|
}
|
2014-11-25 15:54:42 +00:00
|
|
|
|
2015-01-12 21:14:27 +00:00
|
|
|
let sort_field = match matches.opt_str("sort") {
|
2015-05-16 20:02:28 +00:00
|
|
|
Some(word) => try!(SortField::from_word(word)),
|
|
|
|
None => SortField::default(),
|
2015-01-12 21:14:27 +00:00
|
|
|
};
|
|
|
|
|
2015-02-05 14:39:56 +00:00
|
|
|
let filter = FileFilter {
|
2015-02-26 14:05:26 +00:00
|
|
|
list_dirs_first: matches.opt_present("group-directories-first"),
|
2015-02-05 14:39:56 +00:00
|
|
|
reverse: matches.opt_present("reverse"),
|
|
|
|
show_invisibles: matches.opt_present("all"),
|
|
|
|
sort_field: sort_field,
|
|
|
|
};
|
|
|
|
|
|
|
|
let path_strs = if matches.free.is_empty() {
|
|
|
|
vec![ ".".to_string() ]
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
matches.free.clone()
|
|
|
|
};
|
|
|
|
|
2015-02-24 16:05:25 +00:00
|
|
|
let dir_action = try!(DirAction::deduce(&matches));
|
|
|
|
let view = try!(View::deduce(&matches, filter, dir_action));
|
|
|
|
|
2015-02-05 14:39:56 +00:00
|
|
|
Ok((Options {
|
2015-02-24 16:05:25 +00:00
|
|
|
dir_action: dir_action,
|
|
|
|
view: view,
|
2015-02-05 14:39:56 +00:00
|
|
|
filter: filter,
|
|
|
|
}, path_strs))
|
2014-05-25 16:14:50 +00:00
|
|
|
}
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2015-05-12 14:37:59 +00:00
|
|
|
pub fn transform_files(&self, files: &mut Vec<File>) {
|
2015-02-03 17:03:58 +00:00
|
|
|
self.filter.transform_files(files)
|
|
|
|
}
|
2015-08-03 17:44:33 +00:00
|
|
|
|
|
|
|
/// Whether the View specified in this set of options includes a Git
|
|
|
|
/// status column. It's only worth trying to discover a repository if the
|
|
|
|
/// results will end up being displayed.
|
|
|
|
pub fn should_scan_for_git(&self) -> bool {
|
|
|
|
match self.view {
|
|
|
|
View::Details(Details { columns: Some(cols), .. }) => cols.should_scan_for_git(),
|
|
|
|
View::GridDetails(GridDetails { details: Details { columns: Some(cols), .. }, .. }) => cols.should_scan_for_git(),
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
2015-02-03 17:03:58 +00:00
|
|
|
}
|
2015-01-24 13:44:25 +00:00
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
pub struct FileFilter {
|
|
|
|
list_dirs_first: bool,
|
|
|
|
reverse: bool,
|
|
|
|
show_invisibles: bool,
|
|
|
|
sort_field: SortField,
|
|
|
|
}
|
|
|
|
|
2015-02-03 17:03:58 +00:00
|
|
|
impl FileFilter {
|
2015-01-26 01:16:19 +00:00
|
|
|
/// Transform the files (sorting, reversing, filtering) before listing them.
|
2015-05-12 14:37:59 +00:00
|
|
|
pub fn transform_files(&self, files: &mut Vec<File>) {
|
2015-01-24 12:53:25 +00:00
|
|
|
|
|
|
|
if !self.show_invisibles {
|
2015-02-04 15:47:52 +00:00
|
|
|
files.retain(|f| !f.is_dotfile());
|
2015-01-24 12:53:25 +00:00
|
|
|
}
|
2015-01-12 23:31:30 +00:00
|
|
|
|
|
|
|
match self.sort_field {
|
2015-05-12 02:14:56 +00:00
|
|
|
SortField::Unsorted => {},
|
|
|
|
SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
|
2015-05-16 17:16:35 +00:00
|
|
|
SortField::Size => files.sort_by(|a, b| a.metadata.len().cmp(&b.metadata.len())),
|
2015-06-16 23:49:29 +00:00
|
|
|
SortField::FileInode => files.sort_by(|a, b| a.metadata.ino().cmp(&b.metadata.ino())),
|
|
|
|
SortField::ModifiedDate => files.sort_by(|a, b| a.metadata.mtime().cmp(&b.metadata.mtime())),
|
|
|
|
SortField::AccessedDate => files.sort_by(|a, b| a.metadata.atime().cmp(&b.metadata.atime())),
|
|
|
|
SortField::CreatedDate => files.sort_by(|a, b| a.metadata.ctime().cmp(&b.metadata.ctime())),
|
2015-05-12 02:14:56 +00:00
|
|
|
SortField::Extension => files.sort_by(|a, b| match a.ext.cmp(&b.ext) {
|
2015-06-08 20:33:39 +00:00
|
|
|
cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
|
|
|
order => order,
|
2015-01-12 23:31:30 +00:00
|
|
|
}),
|
2014-07-22 19:47:30 +00:00
|
|
|
}
|
2015-01-12 23:31:30 +00:00
|
|
|
|
|
|
|
if self.reverse {
|
|
|
|
files.reverse();
|
|
|
|
}
|
2015-02-26 14:05:26 +00:00
|
|
|
|
|
|
|
if self.list_dirs_first {
|
|
|
|
// This relies on the fact that sort_by is stable.
|
|
|
|
files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory()));
|
|
|
|
}
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
|
|
|
}
|
2014-11-25 20:50:23 +00:00
|
|
|
|
2015-01-31 16:10:40 +00:00
|
|
|
/// User-supplied field to sort by.
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-01-24 12:38:05 +00:00
|
|
|
pub enum SortField {
|
2015-02-22 00:40:41 +00:00
|
|
|
Unsorted, Name, Extension, Size, FileInode,
|
|
|
|
ModifiedDate, AccessedDate, CreatedDate,
|
2015-01-24 12:38:05 +00:00
|
|
|
}
|
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
impl default::Default for SortField {
|
2015-05-16 20:02:28 +00:00
|
|
|
fn default() -> SortField {
|
|
|
|
SortField::Name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-24 12:38:05 +00:00
|
|
|
impl SortField {
|
|
|
|
|
|
|
|
/// Find which field to use based on a user-supplied word.
|
|
|
|
fn from_word(word: String) -> Result<SortField, Misfire> {
|
2015-02-22 17:11:33 +00:00
|
|
|
match &word[..] {
|
2015-05-12 02:14:56 +00:00
|
|
|
"name" | "filename" => Ok(SortField::Name),
|
|
|
|
"size" | "filesize" => Ok(SortField::Size),
|
|
|
|
"ext" | "extension" => Ok(SortField::Extension),
|
|
|
|
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
|
|
|
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
|
|
|
"cr" | "created" => Ok(SortField::CreatedDate),
|
|
|
|
"none" => Ok(SortField::Unsorted),
|
|
|
|
"inode" => Ok(SortField::FileInode),
|
|
|
|
field => Err(SortField::none(field))
|
2015-01-24 12:38:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// How to display an error when the word didn't match with anything.
|
|
|
|
fn none(field: &str) -> Misfire {
|
|
|
|
Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
|
2015-01-24 12:38:05 +00:00
|
|
|
/// One of these things could happen instead of listing files.
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
pub enum Misfire {
|
|
|
|
|
|
|
|
/// The getopts crate didn't like these arguments.
|
|
|
|
InvalidOptions(getopts::Fail),
|
|
|
|
|
|
|
|
/// The user asked for help. This isn't strictly an error, which is why
|
|
|
|
/// this enum isn't named Error!
|
|
|
|
Help(String),
|
|
|
|
|
2015-03-02 14:54:38 +00:00
|
|
|
/// The user wanted the version number.
|
|
|
|
Version,
|
|
|
|
|
2015-02-24 16:05:25 +00:00
|
|
|
/// Two options were given that conflict with one another.
|
2015-01-24 12:38:05 +00:00
|
|
|
Conflict(&'static str, &'static str),
|
|
|
|
|
|
|
|
/// An option was given that does nothing when another one either is or
|
|
|
|
/// isn't present.
|
|
|
|
Useless(&'static str, bool, &'static str),
|
2015-02-24 16:05:25 +00:00
|
|
|
|
2015-02-24 16:19:56 +00:00
|
|
|
/// An option was given that does nothing when either of two other options
|
|
|
|
/// are not present.
|
|
|
|
Useless2(&'static str, &'static str, &'static str),
|
|
|
|
|
2015-02-24 16:05:25 +00:00
|
|
|
/// A numeric option was given that failed to be parsed as a number.
|
|
|
|
FailedParse(ParseIntError),
|
2015-01-24 12:38:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Misfire {
|
|
|
|
/// The OS return code this misfire should signify.
|
2015-02-05 15:25:59 +00:00
|
|
|
pub fn error_code(&self) -> i32 {
|
2015-06-08 20:33:39 +00:00
|
|
|
if let Misfire::Help(_) = *self { 2 }
|
|
|
|
else { 3 }
|
2015-01-24 12:38:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Misfire {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
2015-06-08 20:33:39 +00:00
|
|
|
use self::Misfire::*;
|
|
|
|
|
2015-01-24 12:38:05 +00:00
|
|
|
match *self {
|
2015-05-12 02:14:56 +00:00
|
|
|
InvalidOptions(ref e) => write!(f, "{}", e),
|
|
|
|
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),
|
2015-01-24 12:38:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
|
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
|
|
|
pub enum View {
|
|
|
|
Details(Details),
|
|
|
|
Grid(Grid),
|
2015-06-28 12:21:21 +00:00
|
|
|
GridDetails(GridDetails),
|
|
|
|
Lines(Lines),
|
2015-06-08 20:33:39 +00:00
|
|
|
}
|
|
|
|
|
2015-02-05 14:39:56 +00:00
|
|
|
impl View {
|
2015-02-24 16:05:25 +00:00
|
|
|
pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
|
2015-06-08 20:33:39 +00:00
|
|
|
use self::Misfire::*;
|
|
|
|
|
2015-06-28 12:21:21 +00:00
|
|
|
let long = || {
|
2015-06-29 12:13:23 +00:00
|
|
|
if matches.opt_present("across") && !matches.opt_present("grid") {
|
2015-06-08 20:33:39 +00:00
|
|
|
Err(Useless("across", true, "long"))
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
|
|
|
else if matches.opt_present("oneline") {
|
2015-06-08 20:33:39 +00:00
|
|
|
Err(Useless("oneline", true, "long"))
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
let details = Details {
|
2015-08-03 12:54:25 +00:00
|
|
|
columns: Some(try!(Columns::deduce(matches))),
|
|
|
|
header: matches.opt_present("header"),
|
|
|
|
recurse: dir_action.recurse_options().map(|o| (o, filter)),
|
2015-08-25 17:29:23 +00:00
|
|
|
xattr: xattr::ENABLED && matches.opt_present("extended"),
|
2015-08-03 12:54:25 +00:00
|
|
|
colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
|
2015-02-05 14:39:56 +00:00
|
|
|
};
|
|
|
|
|
2015-06-28 12:21:21 +00:00
|
|
|
Ok(details)
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
2015-06-28 12:21:21 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let long_options_scan = || {
|
2015-08-03 12:54:25 +00:00
|
|
|
for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
|
2015-06-28 12:21:21 +00:00
|
|
|
if matches.opt_present(option) {
|
|
|
|
return Err(Useless(option, false, "long"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg!(feature="git") && matches.opt_present("git") {
|
|
|
|
Err(Useless("git", false, "long"))
|
|
|
|
}
|
2015-08-03 12:54:25 +00:00
|
|
|
else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
|
2015-06-28 19:41:38 +00:00
|
|
|
Err(Useless2("level", "recurse", "tree"))
|
|
|
|
}
|
2015-08-25 17:29:23 +00:00
|
|
|
else if xattr::ENABLED && matches.opt_present("extended") {
|
2015-06-28 19:41:38 +00:00
|
|
|
Err(Useless("extended", false, "long"))
|
|
|
|
}
|
2015-06-28 12:21:21 +00:00
|
|
|
else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let other_options_scan = || {
|
|
|
|
if let Some((width, _)) = dimensions() {
|
|
|
|
if matches.opt_present("oneline") {
|
|
|
|
if matches.opt_present("across") {
|
|
|
|
Err(Useless("across", true, "oneline"))
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
let lines = Lines {
|
|
|
|
colours: Colours::colourful(),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(View::Lines(lines))
|
|
|
|
}
|
2015-05-09 22:57:18 +00:00
|
|
|
}
|
2015-08-03 12:54:25 +00:00
|
|
|
else if matches.opt_present("tree") {
|
|
|
|
let details = Details {
|
|
|
|
columns: None,
|
|
|
|
header: false,
|
|
|
|
recurse: dir_action.recurse_options().map(|o| (o, filter)),
|
|
|
|
xattr: false,
|
|
|
|
colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(View::Details(details))
|
|
|
|
}
|
2015-05-09 22:57:18 +00:00
|
|
|
else {
|
2015-06-28 12:21:21 +00:00
|
|
|
let grid = Grid {
|
|
|
|
across: matches.opt_present("across"),
|
|
|
|
console_width: width,
|
|
|
|
colours: Colours::colourful(),
|
2015-05-09 22:57:18 +00:00
|
|
|
};
|
|
|
|
|
2015-06-28 12:21:21 +00:00
|
|
|
Ok(View::Grid(grid))
|
2015-05-09 22:57:18 +00:00
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
|
|
|
else {
|
2015-06-28 12:21:21 +00:00
|
|
|
// If the terminal width couldn't be matched for some reason, such
|
|
|
|
// as the program's stdout being connected to a file, then
|
|
|
|
// fallback to the lines view.
|
|
|
|
let lines = Lines {
|
|
|
|
colours: Colours::plain(),
|
2015-02-05 14:39:56 +00:00
|
|
|
};
|
|
|
|
|
2015-06-28 12:21:21 +00:00
|
|
|
Ok(View::Lines(lines))
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if matches.opt_present("long") {
|
|
|
|
let long_options = try!(long());
|
|
|
|
|
|
|
|
if matches.opt_present("grid") {
|
|
|
|
match other_options_scan() {
|
|
|
|
Ok(View::Grid(grid)) => return Ok(View::GridDetails(GridDetails { grid: grid, details: long_options })),
|
|
|
|
Ok(lines) => return Ok(lines),
|
|
|
|
Err(e) => return Err(e),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return Ok(View::Details(long_options));
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
2015-05-09 22:57:18 +00:00
|
|
|
}
|
2015-06-28 12:21:21 +00:00
|
|
|
|
|
|
|
try!(long_options_scan());
|
|
|
|
|
|
|
|
other_options_scan()
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
|
|
|
}
|
2014-06-22 07:09:16 +00:00
|
|
|
|
2015-05-09 22:57:18 +00:00
|
|
|
|
2015-02-26 07:18:08 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-05 14:39:56 +00:00
|
|
|
pub enum SizeFormat {
|
|
|
|
DecimalBytes,
|
|
|
|
BinaryBytes,
|
|
|
|
JustBytes,
|
|
|
|
}
|
2014-05-26 17:08:58 +00:00
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
impl default::Default for SizeFormat {
|
2015-05-16 17:33:24 +00:00
|
|
|
fn default() -> SizeFormat {
|
|
|
|
SizeFormat::DecimalBytes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-05 14:39:56 +00:00
|
|
|
impl SizeFormat {
|
|
|
|
pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
|
|
|
|
let binary = matches.opt_present("binary");
|
|
|
|
let bytes = matches.opt_present("bytes");
|
|
|
|
|
|
|
|
match (binary, bytes) {
|
2015-05-12 02:14:56 +00:00
|
|
|
(true, true ) => Err(Misfire::Conflict("binary", "bytes")),
|
|
|
|
(true, false) => Ok(SizeFormat::BinaryBytes),
|
|
|
|
(false, true ) => Ok(SizeFormat::JustBytes),
|
|
|
|
(false, false) => Ok(SizeFormat::DecimalBytes),
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-26 07:18:08 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-09 16:33:27 +00:00
|
|
|
pub enum TimeType {
|
|
|
|
FileAccessed,
|
|
|
|
FileModified,
|
|
|
|
FileCreated,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TimeType {
|
2015-02-09 17:20:42 +00:00
|
|
|
pub fn header(&self) -> &'static str {
|
|
|
|
match *self {
|
2015-05-12 02:14:56 +00:00
|
|
|
TimeType::FileAccessed => "Date Accessed",
|
|
|
|
TimeType::FileModified => "Date Modified",
|
|
|
|
TimeType::FileCreated => "Date Created",
|
2015-02-09 17:20:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-09 17:20:42 +00:00
|
|
|
pub struct TimeTypes {
|
|
|
|
accessed: bool,
|
|
|
|
modified: bool,
|
|
|
|
created: bool,
|
|
|
|
}
|
|
|
|
|
2015-06-08 20:33:39 +00:00
|
|
|
impl default::Default for TimeTypes {
|
2015-05-16 17:33:24 +00:00
|
|
|
fn default() -> TimeTypes {
|
|
|
|
TimeTypes { accessed: false, modified: true, created: false }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-09 17:20:42 +00:00
|
|
|
impl TimeTypes {
|
2015-02-09 16:33:27 +00:00
|
|
|
|
|
|
|
/// Find which field to use based on a user-supplied word.
|
2015-02-09 17:20:42 +00:00
|
|
|
fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
|
2015-02-09 16:33:27 +00:00
|
|
|
let possible_word = matches.opt_str("time");
|
2015-02-09 17:20:42 +00:00
|
|
|
let modified = matches.opt_present("modified");
|
|
|
|
let created = matches.opt_present("created");
|
|
|
|
let accessed = matches.opt_present("accessed");
|
2015-02-09 16:33:27 +00:00
|
|
|
|
|
|
|
if let Some(word) = possible_word {
|
2015-02-09 17:20:42 +00:00
|
|
|
if modified {
|
|
|
|
return Err(Misfire::Useless("modified", true, "time"));
|
|
|
|
}
|
|
|
|
else if created {
|
|
|
|
return Err(Misfire::Useless("created", true, "time"));
|
|
|
|
}
|
|
|
|
else if accessed {
|
|
|
|
return Err(Misfire::Useless("accessed", true, "time"));
|
|
|
|
}
|
|
|
|
|
2015-02-22 17:11:33 +00:00
|
|
|
match &word[..] {
|
2015-02-09 17:20:42 +00:00
|
|
|
"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 }),
|
|
|
|
field => Err(TimeTypes::none(field)),
|
2015-02-09 16:33:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2015-02-09 17:20:42 +00:00
|
|
|
if modified || created || accessed {
|
|
|
|
Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
|
|
|
|
}
|
|
|
|
else {
|
2015-05-16 17:33:24 +00:00
|
|
|
Ok(TimeTypes::default())
|
2015-02-09 17:20:42 +00:00
|
|
|
}
|
2015-02-09 16:33:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// How to display an error when the word didn't match with anything.
|
|
|
|
fn none(field: &str) -> Misfire {
|
|
|
|
Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field)))
|
|
|
|
}
|
|
|
|
}
|
2015-02-09 17:20:42 +00:00
|
|
|
|
2015-02-05 14:39:56 +00:00
|
|
|
/// What to do when encountering a directory?
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-05 14:39:56 +00:00
|
|
|
pub enum DirAction {
|
2015-02-24 16:05:25 +00:00
|
|
|
AsFile,
|
|
|
|
List,
|
|
|
|
Recurse(RecurseOptions),
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl DirAction {
|
|
|
|
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");
|
|
|
|
|
|
|
|
match (recurse, list, tree) {
|
2015-05-12 02:14:56 +00:00
|
|
|
(true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
|
|
|
|
(_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
|
|
|
|
(true, false, false) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
|
|
|
|
(_ , _, true ) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
|
|
|
|
(false, true, _ ) => Ok(DirAction::AsFile),
|
|
|
|
(false, false, _ ) => Ok(DirAction::List),
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
2015-01-31 16:10:40 +00:00
|
|
|
}
|
2015-02-24 16:05:25 +00:00
|
|
|
|
|
|
|
pub fn recurse_options(&self) -> Option<RecurseOptions> {
|
|
|
|
match *self {
|
|
|
|
DirAction::Recurse(opts) => Some(opts),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Use new io + path + fs libraries (LOTS OF CHANGES)
Exa now uses the new IO, Path, and Filesystem libraries that have been out for a while now.
Unfortunately, the new libraries don't *entirely* cover the range of the old libraries just yet: in particular, to become more cross-platform, the data in `UnstableFileStat` isn't available in the Unix `MetadataExt` yet. Much of this is contained in rust-lang/rfcs#1044 (which is due to be implemented in rust-lang/rust#14711), but it's not *entirely* there yet.
As such, this commits a serious loss of functionality: no symlink viewing, no hard links or blocks, or users or groups. Also, some of the code could now be optimised. I just wanted to commit this to sort out most of the 'teething problems' of having a different path system in advance.
Here's an example problem that took ages to fix for you, just because you read this far: when I first got exa to compile, it worked mostly fine, except calling `exa` by itself didn't list the current directory. I traced where the command-line options were being generated, to where files and directories were sorted, to where the threads were spawned... and the problem turned out to be that it was using the full path as the file name, rather than just the last component, and these paths happened to begin with `.`, so it thought they were dotfiles.
2015-04-23 12:00:34 +00:00
|
|
|
pub fn is_as_file(&self) -> bool {
|
|
|
|
match *self {
|
|
|
|
DirAction::AsFile => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-24 16:05:25 +00:00
|
|
|
pub fn is_tree(&self) -> bool {
|
|
|
|
match *self {
|
2015-05-21 15:09:26 +00:00
|
|
|
DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
|
2015-02-24 16:05:25 +00:00
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-24 16:05:25 +00:00
|
|
|
pub struct RecurseOptions {
|
|
|
|
pub tree: bool,
|
|
|
|
pub max_depth: Option<usize>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RecurseOptions {
|
|
|
|
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() {
|
|
|
|
Ok(l) => Some(l),
|
|
|
|
Err(e) => return Err(Misfire::FailedParse(e)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(RecurseOptions {
|
|
|
|
tree: tree,
|
|
|
|
max_depth: max_depth,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_too_deep(&self, depth: usize) -> bool {
|
|
|
|
match self.max_depth {
|
|
|
|
None => false,
|
|
|
|
Some(d) => {
|
|
|
|
d <= depth
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-31 16:10:40 +00:00
|
|
|
}
|
|
|
|
|
2015-05-16 17:33:24 +00:00
|
|
|
#[derive(PartialEq, Copy, Clone, Debug, Default)]
|
2015-01-28 22:02:25 +00:00
|
|
|
pub struct Columns {
|
2015-05-12 02:00:18 +00:00
|
|
|
size_format: SizeFormat,
|
2015-02-09 17:20:42 +00:00
|
|
|
time_types: TimeTypes,
|
2015-01-28 22:02:25 +00:00
|
|
|
inode: bool,
|
|
|
|
links: bool,
|
|
|
|
blocks: bool,
|
|
|
|
group: bool,
|
2015-03-10 17:36:29 +00:00
|
|
|
git: bool
|
2015-01-28 22:02:25 +00:00
|
|
|
}
|
2014-05-26 17:08:58 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
impl Columns {
|
2015-02-05 14:39:56 +00:00
|
|
|
pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
|
2015-01-28 22:02:25 +00:00
|
|
|
Ok(Columns {
|
2015-02-05 14:39:56 +00:00
|
|
|
size_format: try!(SizeFormat::deduce(matches)),
|
2015-02-09 17:20:42 +00:00
|
|
|
time_types: try!(TimeTypes::deduce(matches)),
|
2015-01-28 22:02:25 +00:00
|
|
|
inode: matches.opt_present("inode"),
|
|
|
|
links: matches.opt_present("links"),
|
|
|
|
blocks: matches.opt_present("blocks"),
|
|
|
|
group: matches.opt_present("group"),
|
2015-04-07 01:40:15 +00:00
|
|
|
git: cfg!(feature="git") && matches.opt_present("git"),
|
2015-01-28 22:02:25 +00:00
|
|
|
})
|
2014-05-26 14:44:16 +00:00
|
|
|
}
|
|
|
|
|
2015-08-03 17:44:33 +00:00
|
|
|
pub fn should_scan_for_git(&self) -> bool {
|
|
|
|
self.git
|
|
|
|
}
|
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
|
|
|
let mut columns = vec![];
|
2015-01-12 23:31:30 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
if self.inode {
|
|
|
|
columns.push(Inode);
|
|
|
|
}
|
2014-05-25 18:42:31 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
columns.push(Permissions);
|
2014-05-26 10:50:46 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
if self.links {
|
|
|
|
columns.push(HardLinks);
|
|
|
|
}
|
2014-05-26 10:50:46 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
columns.push(FileSize(self.size_format));
|
2014-05-26 10:50:46 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
if self.blocks {
|
|
|
|
columns.push(Blocks);
|
|
|
|
}
|
2015-01-12 23:31:30 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
columns.push(User);
|
|
|
|
|
|
|
|
if self.group {
|
|
|
|
columns.push(Group);
|
|
|
|
}
|
|
|
|
|
2015-02-09 17:20:42 +00:00
|
|
|
if self.time_types.modified {
|
2015-05-12 02:02:38 +00:00
|
|
|
columns.push(Timestamp(TimeType::FileModified));
|
2015-02-09 17:20:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.time_types.created {
|
2015-05-12 02:02:38 +00:00
|
|
|
columns.push(Timestamp(TimeType::FileCreated));
|
2015-02-09 17:20:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.time_types.accessed {
|
2015-05-12 02:02:38 +00:00
|
|
|
columns.push(Timestamp(TimeType::FileAccessed));
|
2015-02-09 17:20:42 +00:00
|
|
|
}
|
2015-02-09 16:33:27 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
if cfg!(feature="git") {
|
|
|
|
if let Some(d) = dir {
|
2015-08-03 17:44:33 +00:00
|
|
|
if self.should_scan_for_git() && d.has_git_repo() {
|
2015-01-28 22:02:25 +00:00
|
|
|
columns.push(GitStatus);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-27 15:30:55 +00:00
|
|
|
|
2015-01-28 22:02:25 +00:00
|
|
|
columns
|
|
|
|
}
|
2014-05-24 01:17:43 +00:00
|
|
|
}
|
2015-01-12 21:47:05 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::Options;
|
2015-01-24 12:38:05 +00:00
|
|
|
use super::Misfire;
|
2015-08-25 17:29:23 +00:00
|
|
|
use feature::xattr;
|
2015-01-12 21:47:05 +00:00
|
|
|
|
2015-02-05 14:39:56 +00:00
|
|
|
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
|
2015-01-24 12:38:05 +00:00
|
|
|
match misfire {
|
2015-06-08 20:52:46 +00:00
|
|
|
Err(Misfire::Help(_)) => true,
|
|
|
|
_ => false,
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn files() {
|
2015-02-05 14:39:56 +00:00
|
|
|
let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
|
2015-01-31 17:24:54 +00:00
|
|
|
assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn no_args() {
|
2015-02-05 14:39:56 +00:00
|
|
|
let args = Options::getopts(&[]).unwrap().1;
|
2015-01-31 17:24:54 +00:00
|
|
|
assert_eq!(args, vec![ ".".to_string() ])
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2015-01-12 23:31:30 +00:00
|
|
|
fn file_sizes() {
|
|
|
|
let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
|
2015-01-24 12:38:05 +00:00
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn just_binary() {
|
|
|
|
let opts = Options::getopts(&[ "--binary".to_string() ]);
|
2015-01-24 12:38:05 +00:00
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
2015-01-12 23:31:30 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn just_bytes() {
|
|
|
|
let opts = Options::getopts(&[ "--bytes".to_string() ]);
|
2015-01-24 12:38:05 +00:00
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn long_across() {
|
|
|
|
let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
|
2015-01-24 12:38:05 +00:00
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn oneline_across() {
|
|
|
|
let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
|
2015-01-24 12:38:05 +00:00
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
|
2015-01-12 23:31:30 +00:00
|
|
|
}
|
2015-01-24 16:03:58 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn just_header() {
|
|
|
|
let opts = Options::getopts(&[ "--header".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
|
|
|
|
}
|
|
|
|
|
2015-03-10 18:00:52 +00:00
|
|
|
#[test]
|
|
|
|
fn just_group() {
|
|
|
|
let opts = Options::getopts(&[ "--group".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
|
|
|
|
}
|
|
|
|
|
2015-01-24 16:03:58 +00:00
|
|
|
#[test]
|
|
|
|
fn just_inode() {
|
|
|
|
let opts = Options::getopts(&[ "--inode".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn just_links() {
|
|
|
|
let opts = Options::getopts(&[ "--links".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn just_blocks() {
|
|
|
|
let opts = Options::getopts(&[ "--blocks".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
|
|
|
|
}
|
2015-02-21 23:43:40 +00:00
|
|
|
|
2015-03-10 17:56:38 +00:00
|
|
|
#[test]
|
|
|
|
#[cfg(feature="git")]
|
|
|
|
fn just_git() {
|
|
|
|
let opts = Options::getopts(&[ "--git".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
|
|
|
|
}
|
|
|
|
|
2015-02-22 12:26:52 +00:00
|
|
|
#[test]
|
|
|
|
fn extended_without_long() {
|
2015-08-25 17:29:23 +00:00
|
|
|
if xattr::ENABLED {
|
2015-02-22 12:55:13 +00:00
|
|
|
let opts = Options::getopts(&[ "--extended".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
|
|
|
|
}
|
2015-02-22 12:26:52 +00:00
|
|
|
}
|
2015-02-24 16:19:56 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn level_without_recurse_or_tree() {
|
|
|
|
let opts = Options::getopts(&[ "--level".to_string(), "69105".to_string() ]);
|
|
|
|
assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
|
|
|
|
}
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|