summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorBen S <ogham@bsago.me>2015-11-15 18:37:18 +0000
committerBen S <ogham@bsago.me>2015-11-15 18:37:18 +0000
commit021655faecc24b4845afebb7cd61b3d1466a12c7 (patch)
tree9b1adeb4602a8f0f3ea3cae0043b28159cde8f33 /src
parentcc04d0452ffa5583b512bfe38d6d680d181a84b2 (diff)
parentedeec0f6f27304dca1fa4e6c3d2b2355a72ffbb6 (diff)
Merge branch 'better-options'
Diffstat (limited to 'src')
-rw-r--r--src/column.rs92
-rw-r--r--src/file.rs24
-rw-r--r--src/main.rs13
-rw-r--r--src/options.rs583
-rw-r--r--src/output/column.rs231
-rw-r--r--src/output/details.rs33
-rw-r--r--src/output/grid_details.rs3
-rw-r--r--src/output/mod.rs2
8 files changed, 557 insertions, 424 deletions
diff --git a/src/column.rs b/src/column.rs
deleted file mode 100644
index 0da3651..0000000
--- a/src/column.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use ansi_term::Style;
-use unicode_width::UnicodeWidthStr;
-
-use options::{SizeFormat, TimeType};
-
-
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum Column {
- Permissions,
- FileSize(SizeFormat),
- Timestamp(TimeType),
- Blocks,
- User,
- Group,
- HardLinks,
- Inode,
-
- GitStatus,
-}
-
-/// Each column can pick its own **Alignment**. Usually, numbers are
-/// right-aligned, and text is left-aligned.
-#[derive(Copy, Clone)]
-pub enum Alignment {
- Left, Right,
-}
-
-impl Column {
-
- /// Get the alignment this column should use.
- pub fn alignment(&self) -> Alignment {
- match *self {
- Column::FileSize(_) => Alignment::Right,
- Column::HardLinks => Alignment::Right,
- Column::Inode => Alignment::Right,
- Column::Blocks => Alignment::Right,
- Column::GitStatus => Alignment::Right,
- _ => Alignment::Left,
- }
- }
-
- /// Get the text that should be printed at the top, when the user elects
- /// to have a header row printed.
- pub fn header(&self) -> &'static str {
- match *self {
- Column::Permissions => "Permissions",
- Column::FileSize(_) => "Size",
- Column::Timestamp(t) => t.header(),
- Column::Blocks => "Blocks",
- Column::User => "User",
- Column::Group => "Group",
- Column::HardLinks => "Links",
- Column::Inode => "inode",
- Column::GitStatus => "Git",
- }
- }
-}
-
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct Cell {
- pub length: usize,
- pub text: String,
-}
-
-impl Cell {
- pub fn empty() -> Cell {
- Cell {
- text: String::new(),
- length: 0,
- }
- }
-
- pub fn paint(style: Style, string: &str) -> Cell {
- Cell {
- text: style.paint(string).to_string(),
- length: UnicodeWidthStr::width(string),
- }
- }
-
- pub fn add_spaces(&mut self, count: usize) {
- self.length += count;
- for _ in 0 .. count {
- self.text.push(' ');
- }
- }
-
- pub fn append(&mut self, other: &Cell) {
- self.length += other.length;
- self.text.push_str(&*other.text);
- }
-}
diff --git a/src/file.rs b/src/file.rs
index e52ebaa..e851405 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -10,14 +10,13 @@ use std::path::{Component, Path, PathBuf};
use unicode_width::UnicodeWidthStr;
use dir::Dir;
-use options::TimeType;
use self::fields as f;
-// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
-// which is currently unstable and lacks vision for stabilization,
-// see https://github.com/rust-lang/rust/issues/27712
+/// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
+/// which is currently unstable and lacks vision for stabilization,
+/// see https://github.com/rust-lang/rust/issues/27712
#[allow(dead_code)]
mod modes {
use std::os::unix::raw;
@@ -281,15 +280,16 @@ impl<'dir> File<'dir> {
}
}
- /// One of this file's timestamps, as a number in seconds.
- pub fn timestamp(&self, time_type: TimeType) -> f::Time {
- let time_in_seconds = match time_type {
- TimeType::FileAccessed => self.metadata.atime(),
- TimeType::FileModified => self.metadata.mtime(),
- TimeType::FileCreated => self.metadata.ctime(),
- };
+ pub fn modified_time(&self) -> f::Time {
+ f::Time(self.metadata.mtime())
+ }
+
+ pub fn created_time(&self) -> f::Time {
+ f::Time(self.metadata.ctime())
+ }
- f::Time(time_in_seconds)
+ pub fn accessed_time(&self) -> f::Time {
+ f::Time(self.metadata.mtime())
}
/// This file's 'type'.
diff --git a/src/main.rs b/src/main.rs
index 66bd552..8c4761a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -28,7 +28,6 @@ use file::File;
use options::{Options, View};
mod colours;
-mod column;
mod dir;
mod feature;
mod file;
@@ -43,10 +42,14 @@ struct Exa {
}
impl Exa {
- fn run(&mut self, args_file_names: &[String]) {
+ fn run(&mut self, mut args_file_names: Vec<String>) {
let mut files = Vec::new();
let mut dirs = Vec::new();
+ if args_file_names.is_empty() {
+ args_file_names.push(".".to_owned());
+ }
+
for file_name in args_file_names.iter() {
match File::from_path(Path::new(&file_name), None) {
Err(e) => {
@@ -99,8 +102,8 @@ impl Exa {
}
};
- self.options.filter_files(&mut children);
- self.options.sort_files(&mut children);
+ self.options.filter.filter_files(&mut children);
+ self.options.filter.sort_files(&mut children);
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
@@ -146,7 +149,7 @@ fn main() {
match Options::getopts(&args) {
Ok((options, paths)) => {
let mut exa = Exa { options: options };
- exa.run(&paths);
+ exa.run(paths);
},
Err(e) => {
println!("{}", e);
diff --git a/src/options.rs b/src/options.rs
index bf2f4a3..b9828ec 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -7,21 +7,26 @@ use getopts;
use natord;
use colours::Colours;
-use column::Column;
-use column::Column::*;
-use dir::Dir;
use feature::xattr;
use file::File;
use output::{Grid, Details, GridDetails, Lines};
+use output::column::{Columns, TimeTypes, SizeFormat};
use term::dimensions;
-/// The *Options* struct represents a parsed version of the user's
-/// command-line options.
+/// These **options** represent a parsed, error-checked versions of the
+/// user's command-line options.
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Options {
+
+ /// The action to perform when encountering a directory rather than a
+ /// regular file.
pub dir_action: DirAction,
+
+ /// How to sort and filter files before outputting them.
pub filter: FileFilter,
+
+ /// The type of output to use (lines, grid, or details).
pub view: View,
}
@@ -31,39 +36,45 @@ impl Options {
#[allow(unused_results)]
pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
let mut opts = getopts::Options::new();
+
+ opts.optflag("v", "version", "display 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("G", "grid", "display entries in a grid view (default)");
+ opts.optflag("l", "long", "display extended details and attributes");
+ opts.optflag("R", "recurse", "recurse into directories");
+ opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
+ opts.optflag("x", "across", "sort multi-column view entries across");
+
+ // Filtering and sorting options
+ opts.optflag("", "group-directories-first", "list directories before other files");
opts.optflag("a", "all", "show dot-files");
+ opts.optflag("d", "list-dirs", "list directories as regular files");
+ opts.optflag("r", "reverse", "reverse order of files");
+ opts.optopt ("s", "sort", "field to sort by", "WORD");
+
+ // Long view options
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");
- opts.optflag("G", "grid", "display entries in a grid view (default)");
- opts.optflag("", "group-directories-first", "list directories before other files");
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");
- opts.optflag("l", "long", "display extended details and attributes");
opts.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
opts.optflag("m", "modified", "display timestamp of most recent modification");
- 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");
opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
- opts.optflag("T", "tree", "recurse into subdirectories in a tree view");
opts.optflag("u", "accessed", "display timestamp of last access for a file");
opts.optflag("U", "created", "display timestamp of creation for a file");
- opts.optflag("x", "across", "sort multi-column view entries across");
-
- opts.optflag("", "version", "display version of exa");
- opts.optflag("?", "help", "show list of command-line options");
if cfg!(feature="git") {
opts.optflag("", "git", "show git status");
}
if xattr::ENABLED {
- opts.optflag("@", "extended", "display extended attribute keys and sizes in long (-l) output");
+ opts.optflag("@", "extended", "display extended attribute keys and sizes");
}
let matches = match opts.parse(args) {
@@ -72,47 +83,32 @@ impl Options {
};
if matches.opt_present("help") {
- return Err(Misfire::Help(opts.usage("Usage:\n exa [options] [files...]")));
- }
- else if matches.opt_present("version") {
- return Err(Misfire::Version);
- }
-
- let sort_field = match matches.opt_str("sort") {
- Some(word) => try!(SortField::from_word(word)),
- None => SortField::default(),
- };
+ let mut help_string = "Usage:\n exa [options] [files...]\n".to_owned();
- let filter = FileFilter {
- list_dirs_first: matches.opt_present("group-directories-first"),
- reverse: matches.opt_present("reverse"),
- show_invisibles: matches.opt_present("all"),
- sort_field: sort_field,
- };
+ if !matches.opt_present("long") {
+ help_string.push_str(OPTIONS);
+ }
- let path_strs = if matches.free.is_empty() {
- vec![ ".".to_string() ]
- }
- else {
- matches.free.clone()
- };
+ help_string.push_str(LONG_OPTIONS);
- let dir_action = try!(DirAction::deduce(&matches));
- let view = try!(View::deduce(&matches, filter, dir_action));
+ if cfg!(feature="git") {
+ help_string.push_str(GIT_HELP);
+ help_string.push('\n');
+ }
- Ok((Options {
- dir_action: dir_action,
- view: view,
- filter: filter,
- }, path_strs))
- }
+ if xattr::ENABLED {
+ help_string.push_str(EXTENDED_HELP);
+ help_string.push('\n');
+ }
- pub fn sort_files(&self, files: &mut Vec<File>) {
- self.filter.sort_files(files)
- }
+ return Err(Misfire::Help(help_string));
+ }
+ else if matches.opt_present("version") {
+ return Err(Misfire::Version);
+ }
- pub fn filter_files(&self, files: &mut Vec<File>) {
- self.filter.filter_files(files)
+ let options = try!(Options::deduce(&matches));
+ Ok((options, matches.free))
}
/// Whether the View specified in this set of options includes a Git
@@ -127,140 +123,17 @@ impl Options {
}
}
+impl OptionSet for Options {
+ fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
+ let dir_action = try!(DirAction::deduce(&matches));
+ let filter = try!(FileFilter::deduce(&matches));
+ let view = try!(View::deduce(&matches, filter, dir_action));
-#[derive(Default, PartialEq, Debug, Copy, Clone)]
-pub struct FileFilter {
- list_dirs_first: bool,
- reverse: bool,
- show_invisibles: bool,
- sort_field: SortField,
-}
-
-impl FileFilter {
- pub fn filter_files(&self, files: &mut Vec<File>) {
- if !self.show_invisibles {
- files.retain(|f| !f.is_dotfile());
- }
- }
-
- pub fn sort_files(&self, files: &mut Vec<File>) {
- files.sort_by(|a, b| self.compare_files(a, b));
-
- 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.is_directory().cmp(&a.is_directory()));
- }
- }
-
- pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
- match self.sort_field {
- SortField::Unsorted => cmp::Ordering::Equal,
- SortField::Name => natord::compare(&*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::Extension => match a.ext.cmp(&b.ext) {
- cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
- order => order,
- },
- }
- }
-}
-
-/// User-supplied field to sort by.
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum SortField {
- Unsorted, Name, Extension, Size, FileInode,
- ModifiedDate, AccessedDate, CreatedDate,
-}
-
-impl Default for SortField {
- fn default() -> SortField {
- SortField::Name
- }
-}
-
-impl SortField {
-
- /// Find which field to use based on a user-supplied word.
- fn from_word(word: String) -> Result<SortField, Misfire> {
- match &word[..] {
- "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))
- }
- }
-
- /// 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),
-
- /// The user wanted the version number.
- Version,
-
- /// Two options were given that conflict with one another.
- 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),
-
- /// An option was given that does nothing when either of two other options
- /// are not present.
- Useless2(&'static str, &'static str, &'static str),
-
- /// A numeric option was given that failed to be parsed as a number.
- FailedParse(ParseIntError),
-}
-
-impl Misfire {
- /// The OS return code this misfire should signify.
- pub fn error_code(&self) -> i32 {
- if let Misfire::Help(_) = *self { 2 }
- else { 3 }
- }
-}
-
-impl fmt::Display for Misfire {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- use self::Misfire::*;
-
- match *self {
- 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),
- }
+ Ok(Options {
+ dir_action: dir_action,
+ view: view,
+ filter: filter,
+ })
}
}
@@ -274,7 +147,7 @@ pub enum View {
}
impl View {
- pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
+ fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
use self::Misfire::*;
let long = || {
@@ -356,8 +229,8 @@ impl View {
}
}
else {
- // If the terminal width couldn't be matched for some reason, such
- // as the program's stdout being connected to a file, then
+ // 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(),
@@ -389,68 +262,162 @@ impl View {
}
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum SizeFormat {
- DecimalBytes,
- BinaryBytes,
- JustBytes,
+trait OptionSet: Sized {
+ fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire>;
}
-impl Default for SizeFormat {
- fn default() -> SizeFormat {
- SizeFormat::DecimalBytes
+impl OptionSet for Columns {
+ fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
+ Ok(Columns {
+ size_format: try!(SizeFormat::deduce(matches)),
+ time_types: try!(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"),
+ })
}
}
-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) {
- (true, true ) => Err(Misfire::Conflict("binary", "bytes")),
- (true, false) => Ok(SizeFormat::BinaryBytes),
- (false, true ) => Ok(SizeFormat::JustBytes),
- (false, false) => Ok(SizeFormat::DecimalBytes),
+/// 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, Copy, Clone)]
+pub struct FileFilter {
+ list_dirs_first: bool,
+ reverse: bool,
+ show_invisibles: bool,
+ sort_field: SortField,
+}
+
+impl OptionSet for FileFilter {
+ fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
+ let sort_field = try!(SortField::deduce(&matches));
+
+ Ok(FileFilter {
+ list_dirs_first: matches.opt_present("group-directories-first"),
+ reverse: matches.opt_present("reverse"),
+ show_invisibles: matches.opt_present("all"),
+ sort_field: sort_field,
+ })
+ }
+}
+
+impl FileFilter {
+
+ /// Remove every file in the given vector that does *not* pass the
+ /// filter predicate.
+ pub fn filter_files(&self, files: &mut Vec<File>) {
+ if !self.show_invisibles {
+ files.retain(|f| !f.is_dotfile());
+ }
+ }
+
+ /// Sort the files in the given vector based on the sort field option.
+ pub fn sort_files(&self, files: &mut Vec<File>) {
+ files.sort_by(|a, b| self.compare_files(a, b));
+
+ 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.is_directory().cmp(&a.is_directory()));
+ }
+ }
+
+ pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
+ match self.sort_field {
+ SortField::Unsorted => cmp::Ordering::Equal,
+ SortField::Name => natord::compare(&*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::Extension => match a.ext.cmp(&b.ext) {
+ cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
+ order => order,
+ },
}
}
}
+/// User-supplied field to sort by.
#[derive(PartialEq, Debug, Copy, Clone)]
-pub enum TimeType {
- FileAccessed,
- FileModified,
- FileCreated,
+pub enum SortField {
+ Unsorted, Name, Extension, Size, FileInode,
+ ModifiedDate, AccessedDate, CreatedDate,
}
-impl TimeType {
- pub fn header(&self) -> &'static str {
- match *self {
- TimeType::FileAccessed => "Date Accessed",
- TimeType::FileModified => "Date Modified",
- TimeType::FileCreated => "Date Created",
+impl Default for SortField {
+ fn default() -> SortField {
+ SortField::Name
+ }
+}
+
+impl OptionSet for SortField {
+ fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
+ if let Some(word) = matches.opt_str("sort") {
+ match &word[..] {
+ "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(Misfire::bad_argument("sort", field))
+ }
+ }
+ else {
+ Ok(SortField::default())
}
}
}
-#[derive(PartialEq, Debug, Copy, Clone)]
-pub struct TimeTypes {
- accessed: bool,
- modified: bool,
- created: bool,
-}
+impl OptionSet for SizeFormat {
-impl Default for TimeTypes {
- fn default() -> TimeTypes {
- TimeTypes { accessed: false, modified: true, created: false }
+ /// Determine which file size to use in the file size column based on
+ /// the user’s options.
+ ///
+ /// The default mode is to use the decimal prefixes, as they are the
+ /// most commonly-understood, and don’t involve trying to parse large
+ /// 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");
+
+ match (binary, bytes) {
+ (true, true ) => Err(Misfire::Conflict("binary", "bytes")),
+ (true, false) => Ok(SizeFormat::BinaryBytes),
+ (false, true ) => Ok(SizeFormat::JustBytes),
+ (false, false) => Ok(SizeFormat::DecimalBytes),
+ }
}
}
-impl TimeTypes {
- /// Find which field to use based on a user-supplied word.
+impl OptionSet for TimeTypes {
+
+ /// Determine which of a file’s time fields should be displayed for it
+ /// based on the user’s options.
+ ///
+ /// There are two separate ways to pick which fields to show: with a
+ /// flag (such as `--modified`) or with a parameter (such as
+ /// `--time=modified`). An error is signaled if both ways are used.
+ ///
+ /// 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");
@@ -468,11 +435,11 @@ impl TimeTypes {
return Err(Misfire::Useless("accessed", true, "time"));
}
- 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 }),
- field => Err(TimeTypes::none(field)),
+ 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)),
}
}
else {
@@ -484,11 +451,6 @@ impl TimeTypes {
}
}
}
-
- /// 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)))
- }
}
@@ -568,83 +530,106 @@ impl RecurseOptions {
}
-#[derive(PartialEq, Copy, Clone, Debug, Default)]
-pub struct Columns {
- size_format: SizeFormat,
- time_types: TimeTypes,
- inode: bool,
- links: bool,
- blocks: bool,
- group: bool,
- git: bool
-}
-
-impl Columns {
- pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
- Ok(Columns {
- size_format: try!(SizeFormat::deduce(matches)),
- time_types: try!(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"),
- })
- }
-
- pub fn should_scan_for_git(&self) -> bool {
- self.git
- }
+/// One of these things could happen instead of listing files.
+#[derive(PartialEq, Debug)]
+pub enum Misfire {
- pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
- let mut columns = vec![];
+ /// The getopts crate didn't like these arguments.
+ InvalidOptions(getopts::Fail),
- if self.inode {
- columns.push(Inode);
- }
+ /// The user asked for help. This isn't strictly an error, which is why
+ /// this enum isn't named Error!
+ Help(String),
- columns.push(Permissions);
+ /// The user wanted the version number.
+ Version,
- if self.links {
- columns.push(HardLinks);
- }
+ /// Two options were given that conflict with one another.
+ Conflict(&'static str, &'static str),
- columns.push(FileSize(self.size_format));
+ /// An option was given that does nothing when another one either is or
+ /// isn't present.
+ Useless(&'static str, bool, &'static str),
- if self.blocks {
- columns.push(Blocks);
- }
+ /// An option was given that does nothing when either of two other options
+ /// are not present.
+ Useless2(&'static str, &'static str, &'static str),
- columns.push(User);
+ /// A numeric option was given that failed to be parsed as a number.
+ FailedParse(ParseIntError),
+}
- if self.group {
- columns.push(Group);
- }
+impl Misfire {
- if self.time_types.modified {
- columns.push(Timestamp(TimeType::FileModified));
- }
+ /// The OS return code this misfire should signify.
+ pub fn error_code(&self) -> i32 {
+ if let Misfire::Help(_) = *self { 2 }
+ else { 3 }
+ }
- if self.time_types.created {
- columns.push(Timestamp(TimeType::FileCreated));
- }
+ /// The Misfire that happens when an option gets given the wrong
+ /// 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) -> Misfire {
+ Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--{} {}", option, otherwise)))
+ }
+}
- if self.time_types.accessed {
- columns.push(Timestamp(TimeType::FileAccessed));
- }
+impl fmt::Display for Misfire {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use self::Misfire::*;
- if cfg!(feature="git") {
- if let Some(d) = dir {
- if self.should_scan_for_git() && d.has_git_repo() {
- columns.push(GitStatus);
- }
- }
+ match *self {
+ 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),
}
-
- columns
}
}
+static OPTIONS: &'static str = r##"
+DISPLAY OPTIONS
+ -1, --oneline display one entry per line
+ -G, --grid display entries in a grid view (default)
+ -l, --long display extended details and attributes
+ -R, --recurse recurse into directories
+ -T, --tree recurse into subdirectories in a tree view
+ -x, --across sort multi-column view entries across
+
+FILTERING AND SORTING OPTIONS
+ -a, --all show dot-files
+ -d, --list-dirs list directories as regular files
+ -r, --reverse reverse order of files
+ -s, --sort WORD field to sort by
+ --group-directories-first list directories before other files
+"##;
+
+static LONG_OPTIONS: &'static str = r##"
+LONG VIEW OPTIONS
+ -b, --binary use binary prefixes in file sizes
+ -B, --bytes list file sizes in bytes, without prefixes
+ -g, --group show group as well as user
+ -h, --header show a header row at the top
+ -H, --links show number of hard links
+ -i, --inode show each file's inode number
+ -L, --level DEPTH maximum depth of recursion
+ -m, --modified display timestamp of most recent modification
+ -S, --blocks show number of file system blocks
+ -t, --time WORD which timestamp to show for a file
+ -u, --accessed display timestamp of last access for a file
+ -U, --created display timestamp of creation for a file
+"##;
+
+static GIT_HELP: &'static str = r##" -@, --extended display extended attribute keys and sizes"##;
+static EXTENDED_HELP: &'static str = r##" --git show git status for files"##;
+
+
#[cfg(test)]
mod test {
use super::Options;
@@ -679,7 +664,7 @@ mod test {
#[test]
fn no_args() {
let args = Options::getopts(&[]).unwrap().1;
- assert_eq!(args, vec![ ".".to_string() ])
+ assert!(args.is_empty()); // Listing the `.` directory is done in main.rs
}
#[test]
diff --git a/src/output/column.rs b/src/output/column.rs
new f