2015-05-09 22:57:18 +00:00
|
|
|
use colours::Colours;
|
2015-01-28 22:02:25 +00:00
|
|
|
use dir::Dir;
|
2014-05-24 01:17:43 +00:00
|
|
|
use file::File;
|
2015-02-05 14:39:56 +00:00
|
|
|
use column::Column;
|
2014-11-23 21:29:11 +00:00
|
|
|
use column::Column::*;
|
2015-03-26 00:37:12 +00:00
|
|
|
use feature::Attribute;
|
2015-05-09 22:57:18 +00:00
|
|
|
use output::{Grid, Details, Lines};
|
2014-11-23 21:29:11 +00:00
|
|
|
use term::dimensions;
|
|
|
|
|
2015-01-26 01:16:19 +00:00
|
|
|
use std::cmp::Ordering;
|
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-01-24 12:38:05 +00:00
|
|
|
use self::Misfire::*;
|
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
|
|
|
}
|
|
|
|
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-03 17:03:58 +00:00
|
|
|
pub struct FileFilter {
|
2015-02-26 14:05:26 +00:00
|
|
|
list_dirs_first: bool,
|
2015-01-12 18:44:39 +00:00
|
|
|
reverse: bool,
|
|
|
|
show_invisibles: bool,
|
|
|
|
sort_field: SortField,
|
2014-07-06 16:33:40 +00:00
|
|
|
}
|
|
|
|
|
2015-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
2015-02-05 14:39:56 +00:00
|
|
|
pub enum View {
|
|
|
|
Details(Details),
|
2015-05-09 22:57:18 +00:00
|
|
|
Lines(Lines),
|
2015-02-05 14:39:56 +00:00
|
|
|
Grid(Grid),
|
|
|
|
}
|
|
|
|
|
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-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-03-26 00:37:12 +00:00
|
|
|
if Attribute::feature_implemented() {
|
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) {
|
2014-12-12 11:26:18 +00:00
|
|
|
Ok(m) => m,
|
2015-01-24 12:38:05 +00:00
|
|
|
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") {
|
|
|
|
Some(word) => try!(SortField::from_word(word)),
|
|
|
|
None => SortField::Name,
|
|
|
|
};
|
|
|
|
|
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-01-24 13:44:25 +00:00
|
|
|
|
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)),
|
|
|
|
SortField::Size => files.sort_by(|a, b| a.stat.len().cmp(&b.stat.len())),
|
|
|
|
SortField::FileInode => files.sort_by(|a, b| a.stat.as_raw().ino().cmp(&b.stat.as_raw().ino())),
|
|
|
|
SortField::ModifiedDate => files.sort_by(|a, b| a.stat.as_raw().mtime().cmp(&b.stat.as_raw().mtime())),
|
|
|
|
SortField::AccessedDate => files.sort_by(|a, b| a.stat.as_raw().atime().cmp(&b.stat.as_raw().atime())),
|
|
|
|
SortField::CreatedDate => files.sort_by(|a, b| a.stat.as_raw().ctime().cmp(&b.stat.as_raw().ctime())),
|
|
|
|
SortField::Extension => files.sort_by(|a, b| match a.ext.cmp(&b.ext) {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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-01-24 12:38:05 +00:00
|
|
|
if let Help(_) = *self { 2 }
|
|
|
|
else { 3 }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Misfire {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
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-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-02-05 14:39:56 +00:00
|
|
|
if matches.opt_present("long") {
|
|
|
|
if matches.opt_present("across") {
|
|
|
|
Err(Misfire::Useless("across", true, "long"))
|
|
|
|
}
|
|
|
|
else if matches.opt_present("oneline") {
|
|
|
|
Err(Misfire::Useless("oneline", true, "long"))
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
let details = Details {
|
|
|
|
columns: try!(Columns::deduce(matches)),
|
2015-02-09 16:33:27 +00:00
|
|
|
header: matches.opt_present("header"),
|
2015-02-26 07:26:04 +00:00
|
|
|
recurse: dir_action.recurse_options().map(|o| (o, filter)),
|
2015-03-26 00:37:12 +00:00
|
|
|
xattr: Attribute::feature_implemented() && matches.opt_present("extended"),
|
2015-05-09 22:57:18 +00:00
|
|
|
colours: if dimensions().is_some() { Colours::colourful() } else { Colours::plain() },
|
2015-02-05 14:39:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(View::Details(details))
|
|
|
|
}
|
2014-06-22 06:44:00 +00:00
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
else if matches.opt_present("binary") {
|
|
|
|
Err(Misfire::Useless("binary", false, "long"))
|
2014-06-22 06:40:40 +00:00
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
else if matches.opt_present("bytes") {
|
|
|
|
Err(Misfire::Useless("bytes", false, "long"))
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
else if matches.opt_present("inode") {
|
|
|
|
Err(Misfire::Useless("inode", false, "long"))
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
else if matches.opt_present("links") {
|
|
|
|
Err(Misfire::Useless("links", false, "long"))
|
2015-01-12 21:47:05 +00:00
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
else if matches.opt_present("header") {
|
|
|
|
Err(Misfire::Useless("header", false, "long"))
|
|
|
|
}
|
|
|
|
else if matches.opt_present("blocks") {
|
|
|
|
Err(Misfire::Useless("blocks", false, "long"))
|
|
|
|
}
|
2015-03-10 17:56:38 +00:00
|
|
|
else if cfg!(feature="git") && matches.opt_present("git") {
|
|
|
|
Err(Misfire::Useless("git", false, "long"))
|
|
|
|
}
|
2015-02-09 16:55:00 +00:00
|
|
|
else if matches.opt_present("time") {
|
|
|
|
Err(Misfire::Useless("time", false, "long"))
|
|
|
|
}
|
2015-02-10 12:08:01 +00:00
|
|
|
else if matches.opt_present("tree") {
|
|
|
|
Err(Misfire::Useless("tree", false, "long"))
|
|
|
|
}
|
2015-03-10 18:00:52 +00:00
|
|
|
else if matches.opt_present("group") {
|
|
|
|
Err(Misfire::Useless("group", false, "long"))
|
|
|
|
}
|
2015-02-24 16:19:56 +00:00
|
|
|
else if matches.opt_present("level") && !matches.opt_present("recurse") {
|
|
|
|
Err(Misfire::Useless2("level", "recurse", "tree"))
|
|
|
|
}
|
2015-03-26 00:37:12 +00:00
|
|
|
else if Attribute::feature_implemented() && matches.opt_present("extended") {
|
2015-02-22 12:26:52 +00:00
|
|
|
Err(Misfire::Useless("extended", false, "long"))
|
|
|
|
}
|
2015-05-09 22:57:18 +00:00
|
|
|
else if let Some((width, _)) = dimensions() {
|
|
|
|
if matches.opt_present("oneline") {
|
|
|
|
if matches.opt_present("across") {
|
|
|
|
Err(Misfire::Useless("across", true, "oneline"))
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
let lines = Lines {
|
|
|
|
colours: Colours::colourful(),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(View::Lines(lines))
|
|
|
|
}
|
2015-02-05 14:39:56 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
let grid = Grid {
|
|
|
|
across: matches.opt_present("across"),
|
2015-05-09 22:57:18 +00:00
|
|
|
console_width: width,
|
|
|
|
colours: Colours::colourful(),
|
2015-02-05 14:39:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(View::Grid(grid))
|
|
|
|
}
|
2015-05-09 22:57:18 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// 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(),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(View::Lines(lines))
|
2014-06-22 07:09:16 +00:00
|
|
|
}
|
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-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,
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
Ok(TimeTypes { accessed: false, modified: true, created: false })
|
|
|
|
}
|
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 {
|
|
|
|
DirAction::Recurse(RecurseOptions { max_depth: _, tree }) => tree,
|
|
|
|
_ => 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-04-03 22:14:49 +00:00
|
|
|
#[derive(PartialEq, Copy, Clone, Debug)]
|
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-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-03-10 17:36:29 +00:00
|
|
|
if self.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;
|
|
|
|
use super::Misfire::*;
|
2015-03-26 00:37:12 +00:00
|
|
|
use feature::Attribute;
|
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-01-12 21:47:05 +00:00
|
|
|
Err(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))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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-03-26 00:37:12 +00:00
|
|
|
if Attribute::feature_implemented() {
|
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
|
|
|
}
|