Skip to content

Commit

Permalink
Merge branch 'it’s-that-time-again'
Browse files Browse the repository at this point in the history
  • Loading branch information
ogham committed Sep 14, 2017
2 parents 19b7780 + c475ccc commit f55bd6d
Show file tree
Hide file tree
Showing 20 changed files with 187 additions and 107 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ These options are available when running with --long (`-l`):
- **--time-style**: how to format timestamps

- Valid **--color** options are **always**, **automatic**, and **never**.
- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase.
- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**.
- Valid time fields are **modified**, **accessed**, and **created**.
- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**.

Expand Down
2 changes: 1 addition & 1 deletion contrib/completions.bash
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ _exa()
;;

-s|--sort)
COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension modified accessed created type inode none --' -- "$cur" ) )
COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension date time modified accessed created type inode oldest newest age none --' -- "$cur" ) )
return
;;

Expand Down
5 changes: 5 additions & 0 deletions contrib/completions.fish
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -a "1 2
complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order"
complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a "
accessed\t'Sort by file accessed time'
age\t'Sort by file modified time (newest first)'
created\t'Sort by file modified time'
date\t'Sort by file modified time'
ext\t'Sort by file extension'
Ext\t'Sort by file extension (uppercase first)'
extension\t'Sort by file extension'
Expand All @@ -34,8 +36,11 @@ complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a "
modified\t'Sort by file modified time'
name\t'Sort by filename'
Name\t'Sort by filename (uppercase first)'
newest\t'Sort by file modified time (newest first)'
none\t'Do not sort files at all'
oldest\t'Sort by file modified time'
size\t'Sort by file size'
time\t'Sort by file modified time'
type\t'Sort by file type'
"

Expand Down
2 changes: 1 addition & 1 deletion contrib/completions.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ __exa() {
{-d,--list-dirs}"[List directories like regular files]" \
{-L,--level}"+[Limit the depth of recursion]" \
{-r,--reverse}"[Reverse the sort order]" \
{-s,--sort}"[Which field to sort by]:(sort field):(accessed created extension Extension filename Filename inode modified name Name none size type)" \
{-s,--sort}"[Which field to sort by]:(sort field):(accessed age created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \
{-I,--ignore-glob}"[Ignore files that match these glob patterns]" \
{-b,--binary}"[List file sizes with binary prefixes]" \
{-B,--bytes}"[List file sizes in bytes, without any prefixes]" \
Expand Down
1 change: 1 addition & 0 deletions contrib/man/exa.1
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ reverse the sort order
.B \-s, \-\-sort=\f[I]SORT_FIELD\f[]
which field to sort by.
Valid fields are name, Name, extension, Extension, size, modified, accessed, created, inode, type, and none.
The modified field has the aliases date, time, and newest, and its reverse order has the aliases age and oldest.
Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'.
Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'.
.RS
Expand Down
8 changes: 7 additions & 1 deletion src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ fn main() {
},

Err(ref e) if e.is_error() => {
writeln!(stderr(), "{}", e).unwrap();
let mut stderr = stderr();
writeln!(stderr, "{}", e).unwrap();

if let Some(s) = e.suggestion() {
let _ = writeln!(stderr, "{}", s);
}

exit(exits::OPTIONS_ERROR);
},

Expand Down
18 changes: 15 additions & 3 deletions src/fs/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ pub enum SortField {
/// files were created on the filesystem, more or less.
FileInode,

/// The time this file was modified (the “mtime”).
/// The time the 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 file was accessed (the “atime”).
/// The time the was accessed (the “atime”).

This comment has been minimized.

Copy link
@mickael-carl

mickael-carl Sep 18, 2017

Isn't this a typo ? "file" should not have been removed ?

This comment has been minimized.

Copy link
@ogham

ogham Sep 28, 2017

Author Owner

You are correct!

This comment has been minimized.

Copy link
@ogham

ogham Sep 28, 2017

Author Owner

Will be fixed in b829aa3 — thanks for spotting it.

///
/// Oddly enough, this field rarely holds the *actual* accessed time.
/// Recording a read time means writing to the file each time it’s read
Expand All @@ -159,7 +159,7 @@ pub enum SortField {
/// http://unix.stackexchange.com/a/8842
AccessedDate,

/// The time this file was changed or created (the “ctime”).
/// The time the 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.
Expand All @@ -173,6 +173,17 @@ pub enum SortField {
/// Files are ordered according to the `PartialOrd` implementation of
/// `fs::fields::Type`, so changing that will change this.
FileType,

/// The “age” of the file, which is the time it was modified sorted
/// backwards. The reverse of the `ModifiedDate` ordering!
///
/// It turns out that listing the most-recently-modified files first is a
/// common-enough use case that it deserves its own variant. This would be
/// implemented by just using the modified date and setting the reverse
/// flag, but this would make reversing *that* output not work, which is
/// bad, even though that’s kind of nonsensical. So it’s its own variant
/// that can be reversed like usual.
ModifiedAge,
}

/// Whether a field should be sorted case-sensitively or case-insensitively.
Expand Down Expand Up @@ -219,6 +230,7 @@ impl SortField {
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::ModifiedAge => b.metadata.mtime().cmp(&a.metadata.mtime()), // flip b and a

SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes
Ordering::Equal => natord::compare(&*a.name, &*b.name),
Expand Down
31 changes: 18 additions & 13 deletions src/options/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ impl FileFilter {
}
}

const SORTS: &[&str] = &[ "name", "Name", "size", "extension",
"Extension", "modified", "accessed",
"created", "inode", "type", "none" ];

impl SortField {

/// Determines which sort field to use based on the `--sort` argument.
Expand Down Expand Up @@ -53,9 +49,19 @@ impl SortField {
else if word == "Ext" || word == "Extension" {
Ok(SortField::Extension(SortCase::ABCabc))
}
else if word == "mod" || word == "modified" {
else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "new" || word == "newest" {
// “new” sorts oldest at the top and newest at the bottom; “old”
// sorts newest at the top and oldest at the bottom. I think this
// is the right way round to do this: “size” puts the smallest at
// the top and the largest at the bottom, doesn’t it?
Ok(SortField::ModifiedDate)
}
else if word == "age" || word == "old" || word == "oldest" {
// Similarly, “age” means that files with the least age (the
// newest files) get sorted at the top, and files with the most
// age (the oldest) at the bottom.
Ok(SortField::ModifiedAge)
}
else if word == "acc" || word == "accessed" {
Ok(SortField::AccessedDate)
}
Expand All @@ -72,7 +78,7 @@ impl SortField {
Ok(SortField::Unsorted)
}
else {
Err(Misfire::bad_argument(&flags::SORT, word, SORTS))
Err(Misfire::BadArgument(&flags::SORT, word.into()))
}
}
}
Expand Down Expand Up @@ -182,12 +188,6 @@ mod test {
use options::flags;
use options::parser::Flag;

pub fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}

macro_rules! test {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
#[test]
Expand Down Expand Up @@ -216,9 +216,14 @@ mod test {
test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc)));
test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc)));
test!(old: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedDate));
test!(oldest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedDate));
test!(new: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedAge));
test!(newest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedAge));
test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge));

// Errors
test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::bad_argument(&flags::SORT, &os("colour"), super::SORTS)));
test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::BadArgument(&flags::SORT, OsString::from("colour"))));

// Overriding
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
Expand Down
22 changes: 14 additions & 8 deletions src/options/flags.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use options::parser::{Arg, Args, TakesValue};
use options::parser::{Arg, Args, Values, TakesValue};


// exa options
Expand All @@ -14,20 +14,24 @@ pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_valu
pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden };
pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden };

pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary };
pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary };
pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) };
pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) };
const COLOURS: &[&str] = &["always", "auto", "never"];

pub static COLOR_SCALE: Arg = Arg { short: None, long: "color-scale", takes_value: TakesValue::Forbidden };
pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden };

// filtering and sorting options
pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden };
pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden };
pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary };
pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary(None) };
pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden };
pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary };
pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary };
pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary(Some(SORTS)) };
pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary(None) };
pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden };
const SORTS: Values = &[ "name", "Name", "size", "extension",
"Extension", "modified", "accessed",
"created", "inode", "type", "none" ];

// display options
pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden };
Expand All @@ -38,10 +42,12 @@ pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_
pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden };
pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden };
pub static BLOCKS: Arg = Arg { short: Some(b'S'), long: "blocks", takes_value: TakesValue::Forbidden };
pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary };
pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) };
pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden };
pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden };
pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary };
pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) };
const TIMES: Values = &["modified", "accessed", "created"];
const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"];

// optional feature options
pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden };
Expand Down
3 changes: 2 additions & 1 deletion src/options/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ FILTERING AND SORTING OPTIONS
--group-directories-first list directories before other files
-I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore
Valid sort fields: name, Name, extension, Extension, size, type,
modified, accessed, created, inode, none
modified, accessed, created, inode, and none.
date, time, old, and new all refer to modified.
"##;

static LONG_OPTIONS: &str = r##"
Expand Down
75 changes: 49 additions & 26 deletions src/options/misfire.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
use std::ffi::{OsStr, OsString};
use std::ffi::OsString;
use std::fmt;
use std::num::ParseIntError;

use glob;

use options::{HelpString, VersionString};
use options::{flags, HelpString, VersionString};
use options::parser::{Arg, Flag, ParseError};


/// A list of legal choices for an argument-taking option
#[derive(PartialEq, Debug)]
pub struct Choices(&'static [&'static str]);

impl fmt::Display for Choices {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "choices: {}", self.0.join(", "))
}
}

/// 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)]
Expand All @@ -27,7 +17,7 @@ pub enum Misfire {
InvalidOptions(ParseError),

/// The user supplied an illegal choice to an Argument.
BadArgument(&'static Arg, OsString, Choices),
BadArgument(&'static Arg, OsString),

/// The user asked for help. This isn’t strictly an error, which is why
/// this enum isn’t named Error!
Expand Down Expand Up @@ -70,14 +60,6 @@ impl Misfire {
_ => true,
}
}

/// 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: &'static Arg, otherwise: &OsStr, legal: &'static [&'static str]) -> Misfire {
Misfire::BadArgument(option, otherwise.to_os_string(), Choices(legal))
}
}

impl From<glob::PatternError> for Misfire {
Expand All @@ -88,10 +70,18 @@ impl From<glob::PatternError> for Misfire {

impl fmt::Display for Misfire {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use options::parser::TakesValue;
use self::Misfire::*;

match *self {
BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no {:?} setting ({})", a, b, c),
BadArgument(ref arg, ref attempt) => {
if let TakesValue::Necessary(Some(values)) = arg.takes_value {
write!(f, "Option {} has no {:?} setting ({})", arg, attempt, Choices(values))
}
else {
write!(f, "Option {} has no {:?} setting", arg, attempt)
}
},
InvalidOptions(ref e) => write!(f, "{}", e),
Help(ref text) => write!(f, "{}", text),
Version(ref version) => write!(f, "{}", version),
Expand All @@ -113,10 +103,43 @@ impl fmt::Display for ParseError {
use self::ParseError::*;

match *self {
NeedsValue { ref flag } => write!(f, "Flag {} needs a value", flag),
ForbiddenValue { ref flag } => write!(f, "Flag {} cannot take a value", flag),
UnknownShortArgument { ref attempt } => write!(f, "Unknown argument -{}", *attempt as char),
UnknownArgument { ref attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()),
NeedsValue { ref flag, values: None } => write!(f, "Flag {} needs a value", flag),
NeedsValue { ref flag, values: Some(cs) } => write!(f, "Flag {} needs a value ({})", flag, Choices(cs)),
ForbiddenValue { ref flag } => write!(f, "Flag {} cannot take a value", flag),
UnknownShortArgument { ref attempt } => write!(f, "Unknown argument -{}", *attempt as char),
UnknownArgument { ref attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()),
}
}
}

impl Misfire {
/// Try to second-guess what the user was trying to do, depending on what
/// went wrong.
pub fn suggestion(&self) -> Option<&'static str> {
// ‘ls -lt’ and ‘ls -ltr’ are common combinations
if let Misfire::BadArgument(ref time, ref r) = *self {
if *time == &flags::TIME && r == "r" {
return Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\"");
}
}

if let Misfire::InvalidOptions(ParseError::NeedsValue { ref flag, values: _ }) = *self {
if *flag == Flag::Short(b't') {
return Some("To sort newest files last, try \"--sort newest\", or just \"-snew\"");
}
}

None
}
}


/// A list of legal choices for an argument-taking option.
#[derive(PartialEq, Debug)]
pub struct Choices(&'static [&'static str]);

impl fmt::Display for Choices {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "choices: {}", self.0.join(", "))
}
}
Loading

0 comments on commit f55bd6d

Please sign in to comment.