Skip to content

Commit

Permalink
Default to sorting case-insensitively
Browse files Browse the repository at this point in the history
This was touched on in #209 where I got the docs wrong compared to the actual implementation, but after thinking about it, I’d like to switch it round. (The --sort=Name and --sort=name difference has also been switched.) See the big ol’ comment for my reasons.

Because this changes core functionality, it broke many, many tests. You can see that this doesn’t change the -star- tests because the shell, rather than exa, orders the globbed files.

I kept on forgetting which way round Sensitive and Insensitive went, so I named them after the effect they have.
  • Loading branch information
ogham committed Aug 20, 2017
1 parent d716bb7 commit 57c647f
Show file tree
Hide file tree
Showing 25 changed files with 253 additions and 213 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 are case-sensitive.
- 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 time fields are **modified**, **accessed**, and **created**.
- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**.

Expand Down
8 changes: 4 additions & 4 deletions contrib/completions.fish
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a "
accessed\t'Sort by file accessed time'
created\t'Sort by file modified time'
ext\t'Sort by file extension'
Ext\t'Sort by file extension (case-insensitive)'
Ext\t'Sort by file extension (uppercase first)'
extension\t'Sort by file extension'
Extension\t'Sort by file extension (case-insensitive)'
Extension\t'Sort by file extension (uppercase first)'
filename\t'Sort by filename'
Filename\t'Sort by filename (case-insensitive)'
Filename\t'Sort by filename (uppercase first)'
inode\t'Sort by file inode'
modified\t'Sort by file modified time'
name\t'Sort by filename'
Name\t'Sort by filename (case-insensitive)'
Name\t'Sort by filename (uppercase first)'
none\t'Do not sort files at all'
size\t'Sort by file size'
type\t'Sort by file type'
Expand Down
3 changes: 2 additions & 1 deletion contrib/man/exa.1
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ 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.
Fields starting with a capital letter are case-sensitive.
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
.RE
.TP
Expand Down
21 changes: 14 additions & 7 deletions src/fs/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,22 @@ pub enum SortField {

/// Whether a field should be sorted case-sensitively or case-insensitively.
/// This determines which of the `natord` functions to use.
///
/// I kept on forgetting which one was sensitive and which one was
/// insensitive. Would a case-sensitive sort put capital letters first because
/// it takes the case of the letters into account, or intermingle them with
/// lowercase letters because it takes the difference between the two cases
/// into account? I gave up and just named these two variants after the
/// effects they have.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SortCase {

/// Sort files case-sensitively with uppercase first, with ‘A’ coming
/// before ‘a’.
Sensitive,
ABCabc,

/// Sort files case-insensitively, with ‘A’ being equal to ‘a’.
Insensitive,
AaBbCc,
}

impl SortField {
Expand All @@ -199,13 +206,13 @@ impl SortField {
/// together, so `file10` will sort after `file9`, instead of before it
/// because of the `1`.
pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
use self::SortCase::{Sensitive, Insensitive};
use self::SortCase::{ABCabc, AaBbCc};

match *self {
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::Name(ABCabc) => natord::compare(&a.name, &b.name),
SortField::Name(AaBbCc) => 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()),
Expand All @@ -218,12 +225,12 @@ impl SortField {
order => order,
},

SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
SortField::Extension(ABCabc) => 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) {
SortField::Extension(AaBbCc) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
order => order,
},
Expand Down
50 changes: 41 additions & 9 deletions src/options/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ impl SortField {

// The field is an OsStr, so can’t be matched.
if word == "name" || word == "filename" {
Ok(SortField::Name(SortCase::Sensitive))
Ok(SortField::Name(SortCase::AaBbCc))
}
else if word == "Name" || word == "Filename" {
Ok(SortField::Name(SortCase::Insensitive))
Ok(SortField::Name(SortCase::ABCabc))
}
else if word == "size" || word == "filesize" {
Ok(SortField::Size)
}
else if word == "ext" || word == "extension" {
Ok(SortField::Extension(SortCase::Sensitive))
Ok(SortField::Extension(SortCase::AaBbCc))
}
else if word == "Ext" || word == "Extension" {
Ok(SortField::Extension(SortCase::Insensitive))
Ok(SortField::Extension(SortCase::ABCabc))
}
else if word == "mod" || word == "modified" {
Ok(SortField::ModifiedDate)
Expand All @@ -77,9 +77,41 @@ impl SortField {
}
}

// 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
// “Documents” before “apps”, but there’s usually an option to ignore
// characters’ case, putting “apps” before “Documents”.
//
// The argument for following case is that it’s easy to forget whether an item
// begins with an uppercase or lowercase letter and end up having to scan both
// the uppercase and lowercase sub-lists to find the item you want. If you
// happen to pick the sublist it’s not in, it looks like it’s missing, which
// is worse than if you just take longer to find it.
// (https://ux.stackexchange.com/a/79266)
//
// The argument for ignoring case is that it makes exa sort files differently
// from shells. A user would expect a directory’s files to be in the same
// order if they used “exa ~/directory” or “exa ~/directory/*”, but exa sorts
// them in the first case, and the shell in the second case, so they wouldn’t
// be exactly the same if exa does something non-conventional.
//
// However, exa already sorts files differently: it uses natural sorting from
// the natord crate, sorting the string “2” before “10” because the number’s
// smaller, because that’s usually what the user expects to happen. Users will
// name their files with numbers expecting them to be treated like numbers,
// rather than lists of numeric characters.
//
// In the same way, users will name their files with letters expecting the
// order of the letters to matter, rather than each letter’s character’s ASCII
// value. So exa breaks from tradition and ignores case while sorting:
// “apps” first, then “Documents”.
//
// You can get the old behaviour back by sorting with `--sort=Name`.

impl Default for SortField {
fn default() -> SortField {
SortField::Name(SortCase::Sensitive)
SortField::Name(SortCase::AaBbCc)
}
}

Expand All @@ -90,7 +122,7 @@ impl DotFilter {
/// given: one will show dotfiles, but two will show `.` and `..` too.
///
/// It also checks for the `--tree` option in strict mode, because of a
/// special case where `--tree --all --all` won't work: listing the
/// special case where `--tree --all --all` wont work: listing the
/// parent directory in tree mode would loop onto itself!
pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> {
let count = matches.count(&flags::ALL);
Expand Down Expand Up @@ -182,15 +214,15 @@ mod test {
test!(one_arg: SortField <- ["--sort=cr"]; Both => Ok(SortField::CreatedDate));
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::Sensitive)));
test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::Insensitive)));
test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc)));
test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc)));

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

// Overriding
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::Insensitive)));
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc)));
test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
}
Expand Down
2 changes: 1 addition & 1 deletion xtests/files
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
1_bytes 2_bytes 3_bytes 4_bytes 5_bytes 6_bytes 7_bytes 8_bytes 9_bytes 10_bytes 11_bytes 12_bytes 13_bytes
1_KiB 2_KiB 3_KiB 4_KiB 5_KiB 6_KiB 7_KiB 8_KiB 9_KiB 10_KiB 11_KiB 12_KiB 13_KiB
1_MiB 2_MiB 3_MiB 4_MiB 5_MiB 6_MiB 7_MiB 8_MiB 9_MiB 10_MiB 11_MiB 12_MiB 13_MiB
1_bytes 2_bytes 3_bytes 4_bytes 5_bytes 6_bytes 7_bytes 8_bytes 9_bytes 10_bytes 11_bytes 12_bytes 13_bytes
2 changes: 1 addition & 1 deletion xtests/files_120
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
1_bytes 2_bytes 3_bytes 4_bytes 5_bytes 6_bytes 7_bytes 8_bytes 9_bytes 10_bytes 11_bytes 12_bytes 13_bytes
1_KiB 2_KiB 3_KiB 4_KiB 5_KiB 6_KiB 7_KiB 8_KiB 9_KiB 10_KiB 11_KiB 12_KiB 13_KiB
1_MiB 2_MiB 3_MiB 4_MiB 5_MiB 6_MiB 7_MiB 8_MiB 9_MiB 10_MiB 11_MiB 12_MiB 13_MiB
1_bytes 2_bytes 3_bytes 4_bytes 5_bytes 6_bytes 7_bytes 8_bytes 9_bytes 10_bytes 11_bytes 12_bytes 13_bytes
2 changes: 1 addition & 1 deletion xtests/files_160
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
1_bytes 2_bytes 3_bytes 4_bytes 5_bytes 6_bytes 7_bytes 8_bytes 9_bytes 10_bytes 11_bytes 12_bytes 13_bytes
1_KiB 2_KiB 3_KiB 4_KiB 5_KiB 6_KiB 7_KiB 8_KiB 9_KiB 10_KiB 11_KiB 12_KiB 13_KiB
1_MiB 2_MiB 3_MiB 4_MiB 5_MiB 6_MiB 7_MiB 8_MiB 9_MiB 10_MiB 11_MiB 12_MiB 13_MiB
1_bytes 2_bytes 3_bytes 4_bytes 5_bytes 6_bytes 7_bytes 8_bytes 9_bytes 10_bytes 11_bytes 12_bytes 13_bytes
4 changes: 2 additions & 2 deletions xtests/files_200
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
1_KiB 1_bytes 2_MiB 3_KiB 3_bytes 4_MiB 5_KiB 5_bytes 6_MiB 7_KiB 7_bytes 8_MiB 9_KiB 9_bytes 10_MiB 11_KiB 11_bytes 12_MiB 13_KiB 13_bytes
1_MiB 2_KiB 2_bytes 3_MiB 4_KiB 4_bytes 5_MiB 6_KiB 6_bytes 7_MiB 8_KiB 8_bytes 9_MiB 10_KiB 10_bytes 11_MiB 12_KiB 12_bytes 13_MiB
1_bytes 1_MiB 2_KiB 3_bytes 3_MiB 4_KiB 5_bytes 5_MiB 6_KiB 7_bytes 7_MiB 8_KiB 9_bytes 9_MiB 10_KiB 11_bytes 11_MiB 12_KiB 13_bytes 13_MiB
1_KiB 2_bytes 2_MiB 3_KiB 4_bytes 4_MiB 5_KiB 6_bytes 6_MiB 7_KiB 8_bytes 8_MiB 9_KiB 10_bytes 10_MiB 11_KiB 12_bytes 12_MiB 13_KiB
20 changes: 10 additions & 10 deletions xtests/files_40
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
1_KiB 4_MiB 7_bytes 11_KiB
1_MiB 4_bytes 8_KiB 11_MiB
1_bytes 5_KiB 8_MiB 11_bytes
2_KiB 5_MiB 8_bytes 12_KiB
2_MiB 5_bytes 9_KiB 12_MiB
2_bytes 6_KiB 9_MiB 12_bytes
3_KiB 6_MiB 9_bytes 13_KiB
3_MiB 6_bytes 10_KiB 13_MiB
3_bytes 7_KiB 10_MiB 13_bytes
4_KiB 7_MiB 10_bytes
1_bytes 4_KiB 7_MiB 11_bytes
1_KiB 4_MiB 8_bytes 11_KiB
1_MiB 5_bytes 8_KiB 11_MiB
2_bytes 5_KiB 8_MiB 12_bytes
2_KiB 5_MiB 9_bytes 12_KiB
2_MiB 6_bytes 9_KiB 12_MiB
3_bytes 6_KiB 9_MiB 13_bytes
3_KiB 6_MiB 10_bytes 13_KiB
3_MiB 7_bytes 10_KiB 13_MiB
4_bytes 7_KiB 10_MiB
10 changes: 5 additions & 5 deletions xtests/files_80
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
1_KiB 2_bytes 4_MiB 6_KiB 7_bytes 9_MiB 11_KiB 12_bytes
1_MiB 3_KiB 4_bytes 6_MiB 8_KiB 9_bytes 11_MiB 13_KiB
1_bytes 3_MiB 5_KiB 6_bytes 8_MiB 10_KiB 11_bytes 13_MiB
2_KiB 3_bytes 5_MiB 7_KiB 8_bytes 10_MiB 12_KiB 13_bytes
2_MiB 4_KiB 5_bytes 7_MiB 9_KiB 10_bytes 12_MiB
1_bytes 2_MiB 4_KiB 6_bytes 7_MiB 9_KiB 11_bytes 12_MiB
1_KiB 3_bytes 4_MiB 6_KiB 8_bytes 9_MiB 11_KiB 13_bytes
1_MiB 3_KiB 5_bytes 6_MiB 8_KiB 10_bytes 11_MiB 13_KiB
2_bytes 3_MiB 5_KiB 7_bytes 8_MiB 10_KiB 12_bytes 13_MiB
2_KiB 4_bytes 5_MiB 7_KiB 9_bytes 10_MiB 12_KiB
Loading

0 comments on commit 57c647f

Please sign in to comment.