mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-12-27 10:22:40 +00:00
Split up the options module
The original options was becoming a bit unwieldy, and would have been even more so if I added the same amount of comments. So this commit splits it up. There's no extra hiding going on here, or rearranging things within the module: (almost) everything now has to be marked 'pub' to let other sub-modules in the new options module to see it.
This commit is contained in:
parent
b44ae1b56b
commit
e9e1161cec
883
src/options.rs
883
src/options.rs
@ -1,883 +0,0 @@
|
||||
use std::cmp;
|
||||
use std::env::var_os;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use getopts;
|
||||
use natord;
|
||||
|
||||
use fs::feature::xattr;
|
||||
use fs::File;
|
||||
use output::{Grid, Details, GridDetails, Lines};
|
||||
use output::Colours;
|
||||
use output::column::{Columns, TimeTypes, SizeFormat};
|
||||
use term::dimensions;
|
||||
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
|
||||
/// Call getopts on the given slice of command-line strings.
|
||||
#[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");
|
||||
opts.optopt ("", "color", "when to show anything in colours", "WHEN");
|
||||
opts.optopt ("", "colour", "when to show anything in colours (alternate spelling)", "WHEN");
|
||||
|
||||
// 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("g", "group", "show group as well as user");
|
||||
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.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
|
||||
opts.optflag("m", "modified", "display timestamp of most recent modification");
|
||||
opts.optflag("S", "blocks", "show number of file system blocks");
|
||||
opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
|
||||
opts.optflag("u", "accessed", "display timestamp of last access for a file");
|
||||
opts.optflag("U", "created", "display timestamp of creation for a file");
|
||||
|
||||
if cfg!(feature="git") {
|
||||
opts.optflag("", "git", "show git status");
|
||||
}
|
||||
|
||||
if xattr::ENABLED {
|
||||
opts.optflag("@", "extended", "display extended attribute keys and sizes");
|
||||
}
|
||||
|
||||
let matches = match opts.parse(args) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(Misfire::InvalidOptions(e)),
|
||||
};
|
||||
|
||||
if matches.opt_present("help") {
|
||||
let mut help_string = "Usage:\n exa [options] [files...]\n".to_owned();
|
||||
|
||||
if !matches.opt_present("long") {
|
||||
help_string.push_str(OPTIONS);
|
||||
}
|
||||
|
||||
help_string.push_str(LONG_OPTIONS);
|
||||
|
||||
if cfg!(feature="git") {
|
||||
help_string.push_str(GIT_HELP);
|
||||
help_string.push('\n');
|
||||
}
|
||||
|
||||
if xattr::ENABLED {
|
||||
help_string.push_str(EXTENDED_HELP);
|
||||
help_string.push('\n');
|
||||
}
|
||||
|
||||
return Err(Misfire::Help(help_string));
|
||||
}
|
||||
else if matches.opt_present("version") {
|
||||
return Err(Misfire::Version);
|
||||
}
|
||||
|
||||
let options = try!(Options::deduce(&matches));
|
||||
Ok((options, matches.free))
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
Ok(Options {
|
||||
dir_action: dir_action,
|
||||
view: view,
|
||||
filter: filter,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
Details(Details),
|
||||
Grid(Grid),
|
||||
GridDetails(GridDetails),
|
||||
Lines(Lines),
|
||||
}
|
||||
|
||||
impl View {
|
||||
fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
|
||||
use self::Misfire::*;
|
||||
|
||||
let long = || {
|
||||
if matches.opt_present("across") && !matches.opt_present("grid") {
|
||||
Err(Useless("across", true, "long"))
|
||||
}
|
||||
else if matches.opt_present("oneline") {
|
||||
Err(Useless("oneline", true, "long"))
|
||||
}
|
||||
else {
|
||||
let term_colours = try!(TerminalColours::deduce(matches));
|
||||
let colours = match term_colours {
|
||||
TerminalColours::Always => Colours::colourful(),
|
||||
TerminalColours::Never => Colours::plain(),
|
||||
TerminalColours::Automatic => {
|
||||
if dimensions().is_some() {
|
||||
Colours::colourful()
|
||||
}
|
||||
else {
|
||||
Colours::plain()
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let details = Details {
|
||||
columns: Some(try!(Columns::deduce(matches))),
|
||||
header: matches.opt_present("header"),
|
||||
recurse: dir_action.recurse_options(),
|
||||
filter: filter,
|
||||
xattr: xattr::ENABLED && matches.opt_present("extended"),
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(details)
|
||||
}
|
||||
};
|
||||
|
||||
let long_options_scan = || {
|
||||
for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
|
||||
if matches.opt_present(option) {
|
||||
return Err(Useless(option, false, "long"));
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(feature="git") && matches.opt_present("git") {
|
||||
Err(Useless("git", false, "long"))
|
||||
}
|
||||
else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
|
||||
Err(Useless2("level", "recurse", "tree"))
|
||||
}
|
||||
else if xattr::ENABLED && matches.opt_present("extended") {
|
||||
Err(Useless("extended", false, "long"))
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let other_options_scan = || {
|
||||
let term_colours = try!(TerminalColours::deduce(matches));
|
||||
let term_width = try!(TerminalWidth::deduce(matches));
|
||||
|
||||
if let TerminalWidth::Set(width) = term_width {
|
||||
let colours = match term_colours {
|
||||
TerminalColours::Always => Colours::colourful(),
|
||||
TerminalColours::Never => Colours::plain(),
|
||||
TerminalColours::Automatic => Colours::colourful(),
|
||||
};
|
||||
|
||||
if matches.opt_present("oneline") {
|
||||
if matches.opt_present("across") {
|
||||
Err(Useless("across", true, "oneline"))
|
||||
}
|
||||
else {
|
||||
let lines = Lines {
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Lines(lines))
|
||||
}
|
||||
}
|
||||
else if matches.opt_present("tree") {
|
||||
let details = Details {
|
||||
columns: None,
|
||||
header: false,
|
||||
recurse: dir_action.recurse_options(),
|
||||
filter: filter,
|
||||
xattr: false,
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Details(details))
|
||||
}
|
||||
else {
|
||||
let grid = Grid {
|
||||
across: matches.opt_present("across"),
|
||||
console_width: width,
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Grid(grid))
|
||||
}
|
||||
}
|
||||
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 colours = match term_colours {
|
||||
TerminalColours::Always => Colours::colourful(),
|
||||
TerminalColours::Never => Colours::plain(),
|
||||
TerminalColours::Automatic => Colours::plain(),
|
||||
};
|
||||
|
||||
if matches.opt_present("tree") {
|
||||
let details = Details {
|
||||
columns: None,
|
||||
header: false,
|
||||
recurse: dir_action.recurse_options(),
|
||||
filter: filter,
|
||||
xattr: false,
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Details(details))
|
||||
}
|
||||
else {
|
||||
let lines = Lines {
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
try!(long_options_scan());
|
||||
|
||||
other_options_scan()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<'_, F>(&self, files: &mut Vec<F>)
|
||||
where F: AsRef<File<'_>> {
|
||||
|
||||
files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
|
||||
|
||||
if self.reverse {
|
||||
files.reverse();
|
||||
}
|
||||
|
||||
if self.list_dirs_first {
|
||||
// This relies on the fact that `sort_by` is stable.
|
||||
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering {
|
||||
use self::SortCase::{Sensitive, Insensitive};
|
||||
|
||||
match self.sort_field {
|
||||
SortField::Unsorted => cmp::Ordering::Equal,
|
||||
|
||||
SortField::Name(Sensitive) => natord::compare(&a.name, &b.name),
|
||||
SortField::Name(Insensitive) => natord::compare_ignore_case(&a.name, &b.name),
|
||||
|
||||
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
||||
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
||||
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
|
||||
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
|
||||
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
|
||||
|
||||
SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
|
||||
cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
|
||||
cmp::Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// What to do when encountering a directory?
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum DirAction {
|
||||
AsFile,
|
||||
List,
|
||||
Recurse(RecurseOptions),
|
||||
}
|
||||
|
||||
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) {
|
||||
(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),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recurse_options(&self) -> Option<RecurseOptions> {
|
||||
match *self {
|
||||
DirAction::Recurse(opts) => Some(opts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn treat_dirs_as_files(&self) -> bool {
|
||||
match *self {
|
||||
DirAction::AsFile => true,
|
||||
DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// User-supplied field to sort by.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortField {
|
||||
Unsorted,
|
||||
Name(SortCase), Extension(SortCase),
|
||||
Size, FileInode,
|
||||
ModifiedDate, AccessedDate, CreatedDate,
|
||||
}
|
||||
|
||||
/// Whether a field should be sorted case-sensitively or case-insensitively.
|
||||
///
|
||||
/// This determines which of the `natord` functions to use.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortCase {
|
||||
Sensitive,
|
||||
Insensitive,
|
||||
}
|
||||
|
||||
impl Default for SortField {
|
||||
fn default() -> SortField {
|
||||
SortField::Name(SortCase::Sensitive)
|
||||
}
|
||||
}
|
||||
|
||||
impl SortField {
|
||||
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
|
||||
if let Some(word) = matches.opt_str("sort") {
|
||||
match &*word {
|
||||
"name" | "filename" => Ok(SortField::Name(SortCase::Sensitive)),
|
||||
"Name" | "Filename" => Ok(SortField::Name(SortCase::Insensitive)),
|
||||
"size" | "filesize" => Ok(SortField::Size),
|
||||
"ext" | "extension" => Ok(SortField::Extension(SortCase::Sensitive)),
|
||||
"Ext" | "Extension" => Ok(SortField::Extension(SortCase::Insensitive)),
|
||||
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
||||
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
||||
"cr" | "created" => Ok(SortField::CreatedDate),
|
||||
"none" => Ok(SortField::Unsorted),
|
||||
"inode" => Ok(SortField::FileInode),
|
||||
field => Err(Misfire::bad_argument("sort", field))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(SortField::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum TerminalWidth {
|
||||
Set(usize),
|
||||
Unset,
|
||||
}
|
||||
|
||||
impl TerminalWidth {
|
||||
fn deduce(_: &getopts::Matches) -> Result<TerminalWidth, Misfire> {
|
||||
if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
|
||||
match columns.parse() {
|
||||
Ok(width) => Ok(TerminalWidth::Set(width)),
|
||||
Err(e) => Err(Misfire::FailedParse(e)),
|
||||
}
|
||||
}
|
||||
else if let Some((width, _)) = dimensions() {
|
||||
Ok(TerminalWidth::Set(width))
|
||||
}
|
||||
else {
|
||||
Ok(TerminalWidth::Unset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum TerminalColours {
|
||||
Always,
|
||||
Automatic,
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for TerminalColours {
|
||||
fn default() -> TerminalColours {
|
||||
TerminalColours::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalColours {
|
||||
fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
|
||||
if let Some(word) = matches.opt_str("color").or(matches.opt_str("colour")) {
|
||||
match &*word {
|
||||
"always" => Ok(TerminalColours::Always),
|
||||
"auto" | "automatic" => Ok(TerminalColours::Automatic),
|
||||
"never" => Ok(TerminalColours::Never),
|
||||
otherwise => Err(Misfire::bad_argument("color", otherwise))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(TerminalColours::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl 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 {
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// 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");
|
||||
let created = matches.opt_present("created");
|
||||
let accessed = matches.opt_present("accessed");
|
||||
|
||||
if let Some(word) = possible_word {
|
||||
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"));
|
||||
}
|
||||
|
||||
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 if modified || created || accessed {
|
||||
Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
|
||||
}
|
||||
else {
|
||||
Ok(TimeTypes::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
--color, --colour when to colourise the output
|
||||
|
||||
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##" --git show git status for files"##;
|
||||
static EXTENDED_HELP: &'static str = r##" -@, --extended display extended attribute keys and sizes"##;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Options, Misfire, SortField, SortCase};
|
||||
use fs::feature::xattr;
|
||||
|
||||
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
|
||||
match misfire {
|
||||
Err(Misfire::Help(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let opts = Options::getopts(&[ "--help".to_string() ]);
|
||||
assert!(is_helpful(opts))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_with_file() {
|
||||
let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
|
||||
assert!(is_helpful(opts))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files() {
|
||||
let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
|
||||
assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args() {
|
||||
let args = Options::getopts(&[]).unwrap().1;
|
||||
assert!(args.is_empty()); // Listing the `.` directory is done in main.rs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_sizes() {
|
||||
let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_binary() {
|
||||
let opts = Options::getopts(&[ "--binary".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_bytes() {
|
||||
let opts = Options::getopts(&[ "--bytes".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_across() {
|
||||
let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oneline_across() {
|
||||
let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_header() {
|
||||
let opts = Options::getopts(&[ "--header".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_group() {
|
||||
let opts = Options::getopts(&[ "--group".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
|
||||
}
|
||||
|
||||
#[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"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_size() {
|
||||
let opts = Options::getopts(&[ "--sort=size".to_string() ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_name() {
|
||||
let opts = Options::getopts(&[ "--sort=name".to_string() ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_name_lowercase() {
|
||||
let opts = Options::getopts(&[ "--sort=Name".to_string() ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature="git")]
|
||||
fn just_git() {
|
||||
let opts = Options::getopts(&[ "--git".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_without_long() {
|
||||
if xattr::ENABLED {
|
||||
let opts = Options::getopts(&[ "--extended".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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"))
|
||||
}
|
||||
}
|
107
src/options/dir_action.rs
Normal file
107
src/options/dir_action.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use getopts;
|
||||
|
||||
use options::misfire::Misfire;
|
||||
|
||||
|
||||
/// What to do when encountering a directory?
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum DirAction {
|
||||
|
||||
/// This directory should be listed along with the regular files, instead
|
||||
/// of having its contents queried.
|
||||
AsFile,
|
||||
|
||||
/// This directory should not be listed, and should instead be opened and
|
||||
/// *its* files listed separately. This is the default behaviour.
|
||||
List,
|
||||
|
||||
/// This directory should be listed along with the regular files, and then
|
||||
/// its contents should be listed afterward. The recursive contents of
|
||||
/// *those* contents are dictated by the options argument.
|
||||
Recurse(RecurseOptions),
|
||||
}
|
||||
|
||||
impl DirAction {
|
||||
|
||||
/// Determine which action to perform when trying to list a directory.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
|
||||
let recurse = matches.opt_present("recurse");
|
||||
let list = matches.opt_present("list-dirs");
|
||||
let tree = matches.opt_present("tree");
|
||||
|
||||
match (recurse, list, tree) {
|
||||
|
||||
// You can't --list-dirs along with --recurse or --tree because
|
||||
// they already automatically list directories.
|
||||
(true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
|
||||
(_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
|
||||
|
||||
(_ , _, true ) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, true)))),
|
||||
(true, false, false) => Ok(DirAction::Recurse(try!(RecurseOptions::deduce(matches, false)))),
|
||||
(false, true, _ ) => Ok(DirAction::AsFile),
|
||||
(false, false, _ ) => Ok(DirAction::List),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the recurse options, if this dir action has any.
|
||||
pub fn recurse_options(&self) -> Option<RecurseOptions> {
|
||||
match *self {
|
||||
DirAction::Recurse(opts) => Some(opts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to treat directories as regular files or not.
|
||||
pub fn treat_dirs_as_files(&self) -> bool {
|
||||
match *self {
|
||||
DirAction::AsFile => true,
|
||||
DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The options that determine how to recurse into a directory.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct RecurseOptions {
|
||||
|
||||
/// Whether recursion should be done as a tree or as multiple individual
|
||||
/// views of files.
|
||||
pub tree: bool,
|
||||
|
||||
/// The maximum number of times that recursion should descend to, if one
|
||||
/// is specified.
|
||||
pub max_depth: Option<usize>,
|
||||
}
|
||||
|
||||
impl RecurseOptions {
|
||||
|
||||
/// Determine which files should be recursed into.
|
||||
pub fn deduce(matches: &getopts::Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
|
||||
let max_depth = if let Some(level) = matches.opt_str("level") {
|
||||
match level.parse() {
|
||||
Ok(l) => Some(l),
|
||||
Err(e) => return Err(Misfire::FailedParse(e)),
|
||||
}
|
||||
}
|
||||
else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(RecurseOptions {
|
||||
tree: tree,
|
||||
max_depth: max_depth,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether a directory of the given depth would be too deep.
|
||||
pub fn is_too_deep(&self, depth: usize) -> bool {
|
||||
match self.max_depth {
|
||||
None => false,
|
||||
Some(d) => {
|
||||
d <= depth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
228
src/options/filter.rs
Normal file
228
src/options/filter.rs
Normal file
@ -0,0 +1,228 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use getopts;
|
||||
use natord;
|
||||
|
||||
use fs::File;
|
||||
use options::misfire::Misfire;
|
||||
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// Whether directories should be listed first, and other types of file
|
||||
/// second. Some users prefer it like this.
|
||||
pub list_dirs_first: bool,
|
||||
|
||||
/// The metadata field to sort by.
|
||||
pub sort_field: SortField,
|
||||
|
||||
/// Whether to reverse the sorting order. This would sort the largest
|
||||
/// files first, or files starting with Z, or the most-recently-changed
|
||||
/// ones, depending on the sort field.
|
||||
pub reverse: bool,
|
||||
|
||||
/// Whether to include invisible “dot” files when listing a directory.
|
||||
///
|
||||
/// Files starting with a single “.” are used to determine “system” or
|
||||
/// “configuration” files that should not be displayed in a regular
|
||||
/// directory listing.
|
||||
///
|
||||
/// This came about more or less by a complete historical accident,
|
||||
/// when the original `ls` tried to hide `.` and `..`:
|
||||
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
|
||||
///
|
||||
/// When one typed ls, however, these files appeared, so either Ken or
|
||||
/// Dennis added a simple test to the program. It was in assembler then,
|
||||
/// but the code in question was equivalent to something like this:
|
||||
/// if (name[0] == '.') continue;
|
||||
/// This statement was a little shorter than what it should have been,
|
||||
/// which is:
|
||||
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
/// but hey, it was easy.
|
||||
///
|
||||
/// Two things resulted.
|
||||
///
|
||||
/// First, a bad precedent was set. A lot of other lazy programmers
|
||||
/// introduced bugs by making the same simplification. Actual files
|
||||
/// beginning with periods are often skipped when they should be counted.
|
||||
///
|
||||
/// Second, and much worse, the idea of a "hidden" or "dot" file was
|
||||
/// created. As a consequence, more lazy programmers started dropping
|
||||
/// files into everyone's home directory. I don't have all that much
|
||||
/// stuff installed on the machine I'm using to type this, but my home
|
||||
/// directory has about a hundred dot files and I don't even know what
|
||||
/// most of them are or whether they're still needed. Every file name
|
||||
/// evaluation that goes through my home directory is slowed down by
|
||||
/// this accumulated sludge.
|
||||
show_invisibles: bool,
|
||||
}
|
||||
|
||||
impl FileFilter {
|
||||
|
||||
/// Determines the set of file filter options to use, based on the user’s
|
||||
/// command-line arguments.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<'_, F>(&self, files: &mut Vec<F>)
|
||||
where F: AsRef<File<'_>> {
|
||||
|
||||
files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
|
||||
|
||||
if self.reverse {
|
||||
files.reverse();
|
||||
}
|
||||
|
||||
if self.list_dirs_first {
|
||||
// This relies on the fact that `sort_by` is stable.
|
||||
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two files to determine the order they should be listed in,
|
||||
/// depending on the search field.
|
||||
pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
|
||||
use self::SortCase::{Sensitive, Insensitive};
|
||||
|
||||
match self.sort_field {
|
||||
SortField::Unsorted => Ordering::Equal,
|
||||
|
||||
SortField::Name(Sensitive) => natord::compare(&a.name, &b.name),
|
||||
SortField::Name(Insensitive) => natord::compare_ignore_case(&a.name, &b.name),
|
||||
|
||||
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
||||
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
||||
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
|
||||
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
|
||||
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
|
||||
|
||||
SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
|
||||
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
|
||||
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// User-supplied field to sort by.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortField {
|
||||
|
||||
/// Don't apply any sorting. This is usually used as an optimisation in
|
||||
/// scripts, where the order doesn't matter.
|
||||
Unsorted,
|
||||
|
||||
/// The file name. This is the default sorting.
|
||||
Name(SortCase),
|
||||
|
||||
/// The file's extension, with extensionless files being listed first.
|
||||
Extension(SortCase),
|
||||
|
||||
/// The file's size.
|
||||
Size,
|
||||
|
||||
/// The file's inode. This is sometimes analogous to the order in which
|
||||
/// the files were created on the hard drive.
|
||||
FileInode,
|
||||
|
||||
/// The time at which this file was modified (the `mtime`).
|
||||
///
|
||||
/// As this is stored as a Unix timestamp, rather than a local time
|
||||
/// instance, the time zone does not matter and will only be used to
|
||||
/// display the timestamps, not compare them.
|
||||
ModifiedDate,
|
||||
|
||||
/// The time at this file was accessed (the `atime`).
|
||||
///
|
||||
/// Oddly enough, this field rarely holds the *actual* accessed time.
|
||||
/// Recording a read time means writing to the file each time it’s read
|
||||
/// slows the whole operation down, so many systems will only update the
|
||||
/// timestamp in certain circumstances. This has become common enough that
|
||||
/// it’s now expected behaviour for the `atime` field.
|
||||
/// http://unix.stackexchange.com/a/8842
|
||||
AccessedDate,
|
||||
|
||||
/// The time at which this file was changed or created (the `ctime`).
|
||||
///
|
||||
/// Contrary to the name, this field is used to mark the time when a
|
||||
/// file's metadata changed -- its permissions, owners, or link count.
|
||||
///
|
||||
/// In original Unix, this was, however, meant as creation time.
|
||||
/// https://www.bell-labs.com/usr/dmr/www/cacm.html
|
||||
CreatedDate,
|
||||
}
|
||||
|
||||
/// Whether a field should be sorted case-sensitively or case-insensitively.
|
||||
///
|
||||
/// This determines which of the `natord` functions to use.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortCase {
|
||||
|
||||
/// Sort files case-sensitively with uppercase first, with ‘A’ coming
|
||||
/// before ‘a’.
|
||||
Sensitive,
|
||||
|
||||
/// Sort files case-insensitively, with ‘A’ being equal to ‘a’.
|
||||
Insensitive,
|
||||
}
|
||||
|
||||
impl Default for SortField {
|
||||
fn default() -> SortField {
|
||||
SortField::Name(SortCase::Sensitive)
|
||||
}
|
||||
}
|
||||
|
||||
impl SortField {
|
||||
|
||||
/// Determine the sort field to use, based on the presence of a “sort”
|
||||
/// argument. This will return `Err` if the option is there, but does not
|
||||
/// correspond to a valid field.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
|
||||
if let Some(word) = matches.opt_str("sort") {
|
||||
match &*word {
|
||||
"name" | "filename" => Ok(SortField::Name(SortCase::Sensitive)),
|
||||
"Name" | "Filename" => Ok(SortField::Name(SortCase::Insensitive)),
|
||||
"size" | "filesize" => Ok(SortField::Size),
|
||||
"ext" | "extension" => Ok(SortField::Extension(SortCase::Sensitive)),
|
||||
"Ext" | "Extension" => Ok(SortField::Extension(SortCase::Insensitive)),
|
||||
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
||||
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
||||
"cr" | "created" => Ok(SortField::CreatedDate),
|
||||
"none" => Ok(SortField::Unsorted),
|
||||
"inode" => Ok(SortField::FileInode),
|
||||
field => Err(Misfire::bad_argument("sort", field))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(SortField::default())
|
||||
}
|
||||
}
|
||||
}
|
37
src/options/help.rs
Normal file
37
src/options/help.rs
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
pub 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
|
||||
--color, --colour when to colourise the output
|
||||
|
||||
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
|
||||
"##;
|
||||
|
||||
pub 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
|
||||
"##;
|
||||
|
||||
pub static GIT_HELP: &'static str = r##" --git show git status for files"##;
|
||||
pub static EXTENDED_HELP: &'static str = r##" -@, --extended display extended attribute keys and sizes"##;
|
69
src/options/misfire.rs
Normal file
69
src/options/misfire.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use getopts;
|
||||
|
||||
|
||||
/// A **misfire** is a thing that can happen instead of listing files -- a
|
||||
/// catch-all for anything outside the program’s normal execution.
|
||||
#[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 }
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
282
src/options/mod.rs
Normal file
282
src/options/mod.rs
Normal file
@ -0,0 +1,282 @@
|
||||
use getopts;
|
||||
|
||||
use fs::feature::xattr;
|
||||
use output::{Details, GridDetails};
|
||||
|
||||
mod dir_action;
|
||||
pub use self::dir_action::{DirAction, RecurseOptions};
|
||||
|
||||
mod filter;
|
||||
pub use self::filter::{FileFilter, SortField, SortCase};
|
||||
|
||||
mod help;
|
||||
use self::help::*;
|
||||
|
||||
mod misfire;
|
||||
pub use self::misfire::Misfire;
|
||||
|
||||
mod view;
|
||||
pub use self::view::View;
|
||||
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
|
||||
/// Call getopts on the given slice of command-line strings.
|
||||
#[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");
|
||||
opts.optopt ("", "color", "when to show anything in colours", "WHEN");
|
||||
opts.optopt ("", "colour", "when to show anything in colours (alternate spelling)", "WHEN");
|
||||
|
||||
// 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("g", "group", "show group as well as user");
|
||||
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.optopt ("L", "level", "maximum depth of recursion", "DEPTH");
|
||||
opts.optflag("m", "modified", "display timestamp of most recent modification");
|
||||
opts.optflag("S", "blocks", "show number of file system blocks");
|
||||
opts.optopt ("t", "time", "which timestamp to show for a file", "WORD");
|
||||
opts.optflag("u", "accessed", "display timestamp of last access for a file");
|
||||
opts.optflag("U", "created", "display timestamp of creation for a file");
|
||||
|
||||
if cfg!(feature="git") {
|
||||
opts.optflag("", "git", "show git status");
|
||||
}
|
||||
|
||||
if xattr::ENABLED {
|
||||
opts.optflag("@", "extended", "display extended attribute keys and sizes");
|
||||
}
|
||||
|
||||
let matches = match opts.parse(args) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(Misfire::InvalidOptions(e)),
|
||||
};
|
||||
|
||||
if matches.opt_present("help") {
|
||||
let mut help_string = "Usage:\n exa [options] [files...]\n".to_owned();
|
||||
|
||||
if !matches.opt_present("long") {
|
||||
help_string.push_str(OPTIONS);
|
||||
}
|
||||
|
||||
help_string.push_str(LONG_OPTIONS);
|
||||
|
||||
if cfg!(feature="git") {
|
||||
help_string.push_str(GIT_HELP);
|
||||
help_string.push('\n');
|
||||
}
|
||||
|
||||
if xattr::ENABLED {
|
||||
help_string.push_str(EXTENDED_HELP);
|
||||
help_string.push('\n');
|
||||
}
|
||||
|
||||
return Err(Misfire::Help(help_string));
|
||||
}
|
||||
else if matches.opt_present("version") {
|
||||
return Err(Misfire::Version);
|
||||
}
|
||||
|
||||
let options = try!(Options::deduce(&matches));
|
||||
Ok((options, matches.free))
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the complete set of options based on the given command-line
|
||||
/// arguments, after they’ve been parsed.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
|
||||
let dir_action = try!(DirAction::deduce(&matches));
|
||||
let filter = try!(FileFilter::deduce(&matches));
|
||||
let view = try!(View::deduce(&matches, filter, dir_action));
|
||||
|
||||
Ok(Options {
|
||||
dir_action: dir_action,
|
||||
view: view,
|
||||
filter: filter,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Options, Misfire, SortField, SortCase};
|
||||
use fs::feature::xattr;
|
||||
|
||||
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
|
||||
match misfire {
|
||||
Err(Misfire::Help(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let opts = Options::getopts(&[ "--help".to_string() ]);
|
||||
assert!(is_helpful(opts))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_with_file() {
|
||||
let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
|
||||
assert!(is_helpful(opts))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files() {
|
||||
let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
|
||||
assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args() {
|
||||
let args = Options::getopts(&[]).unwrap().1;
|
||||
assert!(args.is_empty()); // Listing the `.` directory is done in main.rs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_sizes() {
|
||||
let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_binary() {
|
||||
let opts = Options::getopts(&[ "--binary".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_bytes() {
|
||||
let opts = Options::getopts(&[ "--bytes".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_across() {
|
||||
let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
|
||||
assert_eq!(opts, Err(Misfire::Useless("across", true, "long")))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oneline_across() {
|
||||
let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
|
||||
assert_eq!(opts, Err(Misfire::Useless("across", true, "oneline")))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_header() {
|
||||
let opts = Options::getopts(&[ "--header".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_group() {
|
||||
let opts = Options::getopts(&[ "--group".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
|
||||
}
|
||||
|
||||
#[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"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_size() {
|
||||
let opts = Options::getopts(&[ "--sort=size".to_string() ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_name() {
|
||||
let opts = Options::getopts(&[ "--sort=name".to_string() ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_name_lowercase() {
|
||||
let opts = Options::getopts(&[ "--sort=Name".to_string() ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature="git")]
|
||||
fn just_git() {
|
||||
let opts = Options::getopts(&[ "--git".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_without_long() {
|
||||
if xattr::ENABLED {
|
||||
let opts = Options::getopts(&[ "--extended".to_string() ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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"))
|
||||
}
|
||||
}
|
355
src/options/view.rs
Normal file
355
src/options/view.rs
Normal file
@ -0,0 +1,355 @@
|
||||
use std::env::var_os;
|
||||
|
||||
use getopts;
|
||||
|
||||
use output::Colours;
|
||||
use output::{Grid, Details, GridDetails, Lines};
|
||||
use options::{FileFilter, DirAction, Misfire};
|
||||
use output::column::{Columns, TimeTypes, SizeFormat};
|
||||
use term::dimensions;
|
||||
use fs::feature::xattr;
|
||||
|
||||
|
||||
/// The **view** contains all information about how to format output.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum View {
|
||||
Details(Details),
|
||||
Grid(Grid),
|
||||
GridDetails(GridDetails),
|
||||
Lines(Lines),
|
||||
}
|
||||
|
||||
impl View {
|
||||
|
||||
/// Determine which view to use and all of that view’s arguments.
|
||||
pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result<View, Misfire> {
|
||||
use options::misfire::Misfire::*;
|
||||
|
||||
let long = || {
|
||||
if matches.opt_present("across") && !matches.opt_present("grid") {
|
||||
Err(Useless("across", true, "long"))
|
||||
}
|
||||
else if matches.opt_present("oneline") {
|
||||
Err(Useless("oneline", true, "long"))
|
||||
}
|
||||
else {
|
||||
let term_colours = try!(TerminalColours::deduce(matches));
|
||||
let colours = match term_colours {
|
||||
TerminalColours::Always => Colours::colourful(),
|
||||
TerminalColours::Never => Colours::plain(),
|
||||
TerminalColours::Automatic => {
|
||||
if dimensions().is_some() {
|
||||
Colours::colourful()
|
||||
}
|
||||
else {
|
||||
Colours::plain()
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let details = Details {
|
||||
columns: Some(try!(Columns::deduce(matches))),
|
||||
header: matches.opt_present("header"),
|
||||
recurse: dir_action.recurse_options(),
|
||||
filter: filter,
|
||||
xattr: xattr::ENABLED && matches.opt_present("extended"),
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(details)
|
||||
}
|
||||
};
|
||||
|
||||
let long_options_scan = || {
|
||||
for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
|
||||
if matches.opt_present(option) {
|
||||
return Err(Useless(option, false, "long"));
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(feature="git") && matches.opt_present("git") {
|
||||
Err(Useless("git", false, "long"))
|
||||
}
|
||||
else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
|
||||
Err(Useless2("level", "recurse", "tree"))
|
||||
}
|
||||
else if xattr::ENABLED && matches.opt_present("extended") {
|
||||
Err(Useless("extended", false, "long"))
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let other_options_scan = || {
|
||||
let term_colours = try!(TerminalColours::deduce(matches));
|
||||
let term_width = try!(TerminalWidth::deduce(matches));
|
||||
|
||||
if let Some(&width) = term_width.as_ref() {
|
||||
let colours = match term_colours {
|
||||
TerminalColours::Always => Colours::colourful(),
|
||||
TerminalColours::Never => Colours::plain(),
|
||||
TerminalColours::Automatic => Colours::colourful(),
|
||||
};
|
||||
|
||||
if matches.opt_present("oneline") {
|
||||
if matches.opt_present("across") {
|
||||
Err(Useless("across", true, "oneline"))
|
||||
}
|
||||
else {
|
||||
let lines = Lines {
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Lines(lines))
|
||||
}
|
||||
}
|
||||
else if matches.opt_present("tree") {
|
||||
let details = Details {
|
||||
columns: None,
|
||||
header: false,
|
||||
recurse: dir_action.recurse_options(),
|
||||
filter: filter,
|
||||
xattr: false,
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Details(details))
|
||||
}
|
||||
else {
|
||||
let grid = Grid {
|
||||
across: matches.opt_present("across"),
|
||||
console_width: width,
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Grid(grid))
|
||||
}
|
||||
}
|
||||
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 colours = match term_colours {
|
||||
TerminalColours::Always => Colours::colourful(),
|
||||
TerminalColours::Never => Colours::plain(),
|
||||
TerminalColours::Automatic => Colours::plain(),
|
||||
};
|
||||
|
||||
if matches.opt_present("tree") {
|
||||
let details = Details {
|
||||
columns: None,
|
||||
header: false,
|
||||
recurse: dir_action.recurse_options(),
|
||||
filter: filter,
|
||||
xattr: false,
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
Ok(View::Details(details))
|
||||
}
|
||||
else {
|
||||
let lines = Lines {
|
||||
colours: colours,
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
try!(long_options_scan());
|
||||
|
||||
other_options_scan()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The width of the terminal requested by the user.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum TerminalWidth {
|
||||
|
||||
/// The user requested this specific number of columns.
|
||||
Set(usize),
|
||||
|
||||
/// The terminal was found to have this number of columns.
|
||||
Terminal(usize),
|
||||
|
||||
/// The user didn’t request any particular terminal width.
|
||||
Unset,
|
||||
}
|
||||
|
||||
impl TerminalWidth {
|
||||
|
||||
/// Determine a requested terminal width from the command-line arguments.
|
||||
///
|
||||
/// Returns an error if a requested width doesn’t parse to an integer.
|
||||
fn deduce(_: &getopts::Matches) -> Result<TerminalWidth, Misfire> {
|
||||
if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
|
||||
match columns.parse() {
|
||||
Ok(width) => Ok(TerminalWidth::Set(width)),
|
||||
Err(e) => Err(Misfire::FailedParse(e)),
|
||||
}
|
||||
}
|
||||
else if let Some((width, _)) = dimensions() {
|
||||
Ok(TerminalWidth::Terminal(width))
|
||||
}
|
||||
else {
|
||||
Ok(TerminalWidth::Unset)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_ref(&self) -> Option<&usize> {
|
||||
match *self {
|
||||
TerminalWidth::Set(ref width) => Some(width),
|
||||
TerminalWidth::Terminal(ref width) => Some(width),
|
||||
TerminalWidth::Unset => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl 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 {
|
||||
|
||||
/// 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 {
|
||||
|
||||
/// 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");
|
||||
let created = matches.opt_present("created");
|
||||
let accessed = matches.opt_present("accessed");
|
||||
|
||||
if let Some(word) = possible_word {
|
||||
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"));
|
||||
}
|
||||
|
||||
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 if modified || created || accessed {
|
||||
Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
|
||||
}
|
||||
else {
|
||||
Ok(TimeTypes::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Under what circumstances we should display coloured, rather than plain,
|
||||
/// output to the terminal.
|
||||
///
|
||||
/// By default, we want to display the colours when stdout can display them.
|
||||
/// Turning them on when output is going to, say, a pipe, would make programs
|
||||
/// such as `grep` or `more` not work properly. So the `Automatic` mode does
|
||||
/// this check and only displays colours when they can be truly appreciated.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum TerminalColours {
|
||||
|
||||
/// Display them even when output isn’t going to a terminal.
|
||||
Always,
|
||||
|
||||
/// Display them when output is going to a terminal, but not otherwise.
|
||||
Automatic,
|
||||
|
||||
/// Never display them, even when output is going to a terminal.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for TerminalColours {
|
||||
fn default() -> TerminalColours {
|
||||
TerminalColours::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalColours {
|
||||
|
||||
/// Determine which terminal colour conditions to use.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
|
||||
if let Some(word) = matches.opt_str("color").or(matches.opt_str("colour")) {
|
||||
match &*word {
|
||||
"always" => Ok(TerminalColours::Always),
|
||||
"auto" | "automatic" => Ok(TerminalColours::Automatic),
|
||||
"never" => Ok(TerminalColours::Never),
|
||||
otherwise => Err(Misfire::bad_argument("color", otherwise))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(TerminalColours::default())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user