diff options
author | Ben S <ogham@bsago.me> | 2015-11-15 18:37:18 +0000 |
---|---|---|
committer | Ben S <ogham@bsago.me> | 2015-11-15 18:37:18 +0000 |
commit | 021655faecc24b4845afebb7cd61b3d1466a12c7 (patch) | |
tree | 9b1adeb4602a8f0f3ea3cae0043b28159cde8f33 /src | |
parent | cc04d0452ffa5583b512bfe38d6d680d181a84b2 (diff) | |
parent | edeec0f6f27304dca1fa4e6c3d2b2355a72ffbb6 (diff) |
Merge branch 'better-options'
Diffstat (limited to 'src')
-rw-r--r-- | src/column.rs | 92 | ||||
-rw-r--r-- | src/file.rs | 24 | ||||
-rw-r--r-- | src/main.rs | 13 | ||||
-rw-r--r-- | src/options.rs | 583 | ||||
-rw-r--r-- | src/output/column.rs | 231 | ||||
-rw-r--r-- | src/output/details.rs | 33 | ||||
-rw-r--r-- | src/output/grid_details.rs | 3 | ||||
-rw-r--r-- | src/output/mod.rs | 2 |
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 |