Skip to content

Commit

Permalink
Merge pull request #457 from ariasuni/fix-sorting-by-created-time
Browse files Browse the repository at this point in the history
Fix sorting by created time
  • Loading branch information
ogham authored Jul 15, 2019
2 parents b767c21 + f0e7321 commit faed8f9
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 143 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ 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. 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 sort fields are **accessed**, **changed**, **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**, **changed**, **accessed**, and **created**.
- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**.


Expand Down
4 changes: 2 additions & 2 deletions contrib/completions.bash
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ _exa()
;;

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

-t|--time)
COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) )
COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) )
return
;;

Expand Down
7 changes: 5 additions & 2 deletions contrib/completions.fish
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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)'
changed\t'Sort by changed time'
created\t'Sort by file modified time'
date\t'Sort by file modified time'
ext\t'Sort by file extension'
Expand Down Expand Up @@ -54,13 +55,15 @@ complete -c exa -s 'g' -l 'group' -d "List each file's group"
complete -c exa -s 'h' -l 'header' -d "Add a header row to each column"
complete -c exa -s 'h' -l 'links' -d "List each file's number of hard links"
complete -c exa -s 'g' -l 'group' -d "List each file's inode number"
complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field"
complete -c exa -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks"
complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a "
modified\t'Display modified time'
changed\t'Display changed time'
accessed\t'Display accessed time'
created\t'Display created time'
modified\t'Display modified time'
"
complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field"
complete -c exa -l 'changed' -d "Use the changed timestamp field"
complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field"
complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field"
complete -c exa -l 'time-style' -x -d "How to format timestamps" -a "
Expand Down
4 changes: 2 additions & 2 deletions contrib/completions.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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 age created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \
{-s,--sort}="[Which field to sort by]:(sort field):(accessed age changed 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 All @@ -37,7 +37,7 @@ __exa() {
{-i,--inode}"[List each file's inode number]" \
{-m,--modified}"[Use the modified timestamp field]" \
{-S,--blocks}"[List each file's number of filesystem blocks]" \
{-t,--time}="[Which time field to show]:(time field):(accessed created modified)" \
{-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \
--time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso)" \
{-u,--accessed}"[Use the accessed timestamp field]" \
{-U,--created}"[Use the created timestamp field]" \
Expand Down
6 changes: 3 additions & 3 deletions contrib/man/exa.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.hy
.TH "exa" "1" "2017\-07\-07" "exa 0.7.0" ""
.TH "exa" "1" "2018\-12\-17" "exa 0.9.0" ""
.SH NAME
.PP
exa \- a modern replacement for ls
Expand Down Expand Up @@ -86,7 +86,7 @@ reverse the sort order
.TP
.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.
Valid fields are name, Name, extension, Extension, size, modified, changed, 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'.
Expand Down Expand Up @@ -158,7 +158,7 @@ list each file\[aq]s number of file system blocks
.RE
.TP
.B \-t, \-\-time=\f[I]WORD\f[]
which timestamp field to list (modified, accessed, created)
which timestamp field to list (modified, changed, accessed, created)
.RS
.RE
.TP
Expand Down
67 changes: 50 additions & 17 deletions src/fs/file.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! Files, and methods and fields to access their metadata.
use std::fs;
use std::fs::{self, metadata};
use std::io::Error as IOError;
use std::io::Result as IOResult;
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
use std::path::{Path, PathBuf};
use std::time::{UNIX_EPOCH, Duration};

use fs::dir::Dir;
use fs::fields as f;
use options::Misfire;


/// A **File** is a wrapper around one of Rust's Path objects, along with
Expand Down Expand Up @@ -325,27 +327,23 @@ impl<'dir> File<'dir> {
}

/// This file’s last modified timestamp.
pub fn modified_time(&self) -> f::Time {
f::Time {
seconds: self.metadata.mtime(),
nanoseconds: self.metadata.mtime_nsec()
}
pub fn modified_time(&self) -> Duration {
self.metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap()
}

/// This file’s created timestamp.
pub fn created_time(&self) -> f::Time {
f::Time {
seconds: self.metadata.ctime(),
nanoseconds: self.metadata.ctime_nsec()
}
/// This file’s last changed timestamp.
pub fn changed_time(&self) -> Duration {
Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32)
}

/// This file’s last accessed timestamp.
pub fn accessed_time(&self) -> f::Time {
f::Time {
seconds: self.metadata.atime(),
nanoseconds: self.metadata.atime_nsec()
}
pub fn accessed_time(&self) -> Duration {
self.metadata.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap()
}

/// This file’s created timestamp.
pub fn created_time(&self) -> Duration {
self.metadata.created().unwrap().duration_since(UNIX_EPOCH).unwrap()
}

/// This file’s ‘type’.
Expand Down Expand Up @@ -462,6 +460,41 @@ impl<'dir> FileTarget<'dir> {
}


pub enum PlatformMetadata {
ModifiedTime,
ChangedTime,
AccessedTime,
CreatedTime,
}

impl PlatformMetadata {
pub fn check_supported(&self) -> Result<(), Misfire> {
use std::env::temp_dir;
let result = match self {
// Call the functions that return a Result to see if it works
PlatformMetadata::AccessedTime => metadata(temp_dir()).unwrap().accessed(),
PlatformMetadata::ModifiedTime => metadata(temp_dir()).unwrap().modified(),
PlatformMetadata::CreatedTime => metadata(temp_dir()).unwrap().created(),
// We use the Unix API so we know it’s not available elsewhere
PlatformMetadata::ChangedTime => {
if cfg!(target_family = "unix") {
return Ok(())
} else {
return Err(Misfire::Unsupported(
// for consistency, this error message similar to the one Rust
// use when created time is not available
"status modified time is not available on this platform currently".to_string()));
}
},
};
match result {
Ok(_) => Ok(()),
Err(err) => Err(Misfire::Unsupported(err.to_string()))
}
}
}


/// More readable aliases for the permission bits exposed by libc.
#[allow(trivial_numeric_casts)]
mod modes {
Expand Down
10 changes: 7 additions & 3 deletions src/fs/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,16 @@ pub enum SortField {
/// http://unix.stackexchange.com/a/8842
AccessedDate,

/// The time the file was changed or created (the “ctime”).
/// The time the file was changed (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.
/// 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
ChangedDate,

/// The time the file was created (the "btime" or "birthtime").
CreatedDate,

/// The type of the file: directories, links, pipes, regular, files, etc.
Expand Down Expand Up @@ -247,6 +250,7 @@ impl SortField {
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
SortField::ModifiedDate => a.modified_time().cmp(&b.modified_time()),
SortField::AccessedDate => a.accessed_time().cmp(&b.accessed_time()),
SortField::ChangedDate => a.changed_time().cmp(&b.changed_time()),
SortField::CreatedDate => a.created_time().cmp(&b.created_time()),
SortField::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a

Expand Down
2 changes: 1 addition & 1 deletion src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod dir;
pub use self::dir::{Dir, DotFilter};

mod file;
pub use self::file::{File, FileTarget};
pub use self::file::{File, FileTarget, PlatformMetadata};

pub mod feature;
pub mod fields;
Expand Down
89 changes: 43 additions & 46 deletions src/options/filter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Parsing the options for `FileFilter`.
use fs::DotFilter;
use fs::{DotFilter, PlatformMetadata};
use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};

use options::{flags, Misfire};
Expand Down Expand Up @@ -35,62 +35,59 @@ impl SortField {
None => return Ok(SortField::default()),
};

// The field is an OsStr, so can’t be matched.
if word == "name" || word == "filename" {
Ok(SortField::Name(SortCase::AaBbCc))
}
else if word == "Name" || word == "Filename" {
Ok(SortField::Name(SortCase::ABCabc))
}
else if word == ".name" || word == ".filename" {
Ok(SortField::NameMixHidden(SortCase::AaBbCc))
}
else if word == ".Name" || word == ".Filename" {
Ok(SortField::NameMixHidden(SortCase::ABCabc))
}
else if word == "size" || word == "filesize" {
Ok(SortField::Size)
}
else if word == "ext" || word == "extension" {
Ok(SortField::Extension(SortCase::AaBbCc))
}
else if word == "Ext" || word == "Extension" {
Ok(SortField::Extension(SortCase::ABCabc))
}
else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "new" || word == "newest" {
// Get String because we can’t match an OsStr
let word = match word.to_str() {
Some(ref w) => *w,
None => return Err(Misfire::BadArgument(&flags::SORT, word.into()))
};

let field = match word {
"name" | "filename" => SortField::Name(SortCase::AaBbCc),
"Name" | "Filename" => SortField::Name(SortCase::ABCabc),
".name" | ".filename" => SortField::NameMixHidden(SortCase::AaBbCc),
".Name" | ".Filename" => SortField::NameMixHidden(SortCase::ABCabc),
"size" | "filesize" => SortField::Size,
"ext" | "extension" => SortField::Extension(SortCase::AaBbCc),
"Ext" | "Extension" => SortField::Extension(SortCase::ABCabc),
// “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" {
"date" | "time" | "mod" | "modified" | "new" | "newest" => SortField::ModifiedDate,
// 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)
}
else if word == "cr" || word == "created" {
Ok(SortField::CreatedDate)
}
else if word == "inode" {
Ok(SortField::FileInode)
}
else if word == "type" {
Ok(SortField::FileType)
}
else if word == "none" {
Ok(SortField::Unsorted)
"age" | "old" | "oldest" => SortField::ModifiedAge,
"ch" | "changed" => SortField::ChangedDate,
"acc" | "accessed" => SortField::AccessedDate,
"cr" | "created" => SortField::CreatedDate,
"inode" => SortField::FileInode,
"type" => SortField::FileType,
"none" => SortField::Unsorted,
_ => return Err(Misfire::BadArgument(&flags::SORT, word.into()))
};

match SortField::to_platform_metadata(field) {
Some(m) => match m.check_supported() {
Ok(_) => Ok(field),
Err(misfire) => Err(misfire),
},
None => Ok(field),
}
else {
Err(Misfire::BadArgument(&flags::SORT, word.into()))
}

fn to_platform_metadata(field: Self) -> Option<PlatformMetadata> {
match field {
SortField::ModifiedDate => Some(PlatformMetadata::ModifiedTime),
SortField::ChangedDate => Some(PlatformMetadata::ChangedTime),
SortField::AccessedDate => Some(PlatformMetadata::AccessedTime),
SortField::CreatedDate => Some(PlatformMetadata::CreatedTime),
_ => None
}
}
}


// I’ve gone back and forth between whether to sort case-sensitively or
// insensitively by default. The default string sort in most programming
// languages takes each character’s ASCII value into account, sorting
Expand Down Expand Up @@ -227,7 +224,7 @@ mod test {
test!(empty: SortField <- []; Both => Ok(SortField::default()));

// Sort field arguments
test!(one_arg: SortField <- ["--sort=cr"]; Both => Ok(SortField::CreatedDate));
test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate));
test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size));
test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc)));
Expand Down
10 changes: 5 additions & 5 deletions src/options/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", t
pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden };
pub static ONLY_DIRS: Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden };
const SORTS: Values = &[ "name", "Name", "size", "extension",
"Extension", "modified", "accessed",
"Extension", "modified", "changed", "accessed",
"created", "inode", "type", "none" ];

// display options
Expand All @@ -43,12 +43,13 @@ pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_
pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden };
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 CHANGED: Arg = Arg { short: None, long: "changed", 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(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(Some(TIME_STYLES)) };
const TIMES: Values = &["modified", "accessed", "created"];
const TIMES: Values = &["modified", "changed", "accessed", "created"];
const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"];

// optional feature options
Expand All @@ -65,9 +66,8 @@ pub static ALL_ARGS: Args = Args(&[
&ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
&IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS,

&BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &BLOCKS,
&TIME, &ACCESSED, &CREATED, &TIME_STYLE,
&BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &CHANGED,
&BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,

&GIT, &EXTENDED,
]);

Loading

0 comments on commit faed8f9

Please sign in to comment.