Repository: lsd-rs/lsd (Rust)

Issue:
Add support/parameter for "natural sort"
## Expected behavior
The "lsd" command should have a nice parameter wich naturally sorts numbers within text.

## Actual behavior
The "ls" command has a nice parameter, but "lsd" hasn't:  
-v     natural sort of (version) numbers within text



Discussion:
I could help to implement this, 
but first I would like to make sure how this work because I was confused about the description `(version) numbers within text`

what's the meaning of the `(version)`?

AFAIK, `-v` should sort by file name in natural order, like 
1. numbers before letters
2. number order in the natural order, like 1 2 11 21

Hi @zwpaper , great, that you could help :-)
Normal "ls" has "-v" option which ends up in what you said in both points:
(but I don't know why a capital letter should be sorted before a lower case and I won't mind/care about this)
```
root@ubuntu-cx11-02:~/temp/1# command ls -lv #h #--time-style=+%Y-%m-%d_%H:%M:%S
total 0
-rw-r--r-- 1 root root 0 Aug 12 09:12 1
-rw-r--r-- 1 root root 0 Aug 12 09:12 2
-rw-r--r-- 1 root root 0 Aug 12 09:12 3
-rw-r--r-- 1 root root 0 Aug 12 09:12 4
-rw-r--r-- 1 root root 0 Aug 12 09:12 10
-rw-r--r-- 1 root root 0 Aug 12 09:12 11
-rw-r--r-- 1 root root 0 Aug 12 09:12 12
-rw-r--r-- 1 root root 0 Aug 12 09:12 20
-rw-r--r-- 1 root root 0 Aug 12 09:12 21
-rw-r--r-- 1 root root 0 Aug 12 09:12 A
-rw-r--r-- 1 root root 0 Aug 12 09:12 B
-rw-r--r-- 1 root root 0 Aug 12 09:12 C
-rw-r--r-- 1 root root 0 Aug 12 09:12 a
-rw-r--r-- 1 root root 0 Aug 12 09:12 b
-rw-r--r-- 1 root root 0 Aug 12 09:12 c
```
instead of this without "-v":
```
root@ubuntu-cx11-02:~/temp/1# command ls -l
total 0
-rw-r--r-- 1 root root 0 Aug 12 09:12 1
-rw-r--r-- 1 root root 0 Aug 12 09:12 10
-rw-r--r-- 1 root root 0 Aug 12 09:12 11
-rw-r--r-- 1 root root 0 Aug 12 09:12 12
-rw-r--r-- 1 root root 0 Aug 12 09:12 2
-rw-r--r-- 1 root root 0 Aug 12 09:12 20
-rw-r--r-- 1 root root 0 Aug 12 09:12 21
-rw-r--r-- 1 root root 0 Aug 12 09:12 3
-rw-r--r-- 1 root root 0 Aug 12 09:12 4
-rw-r--r-- 1 root root 0 Aug 12 09:12 a
-rw-r--r-- 1 root root 0 Aug 12 09:12 A
-rw-r--r-- 1 root root 0 Aug 12 09:12 b
-rw-r--r-- 1 root root 0 Aug 12 09:12 B
-rw-r--r-- 1 root root 0 Aug 12 09:12 c
-rw-r--r-- 1 root root 0 Aug 12 09:12 C
```




Patch:
diff --git a/Cargo.lock b/Cargo.lock
index eddc5e52a..c83500557 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -201,6 +201,11 @@ dependencies = [
  "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "human-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "ignore"
 version = "0.4.14"
@@ -264,6 +269,7 @@ dependencies = [
  "chrono-humanize 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "globset 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "human-sort 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)",
  "lscolors 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "predicates 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -609,6 +615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum globset 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120"
 "checksum globwalk 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d9db17aec586697a93219b19726b5b68307eba92898c34b170857343fe67c99d"
 "checksum hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e"
+"checksum human-sort 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219"
 "checksum ignore 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "ddf60d063dbe6b75388eec66cfc07781167ae3d34a09e0c433e6c5de0511f7fb"
 "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
 "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
diff --git a/Cargo.toml b/Cargo.toml
index 42e02392c..2ebcccf11 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ unicode-width = "0.1.*"
 lscolors = "0.7"
 wild = "2.0.*"
 globset = "0.4.*"
+human-sort = "0.2.2"
 
 [target.'cfg(unix)'.dependencies]
 users = "0.10.*"
diff --git a/src/app.rs b/src/app.rs
index a446ce664..e6a4fb736 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -146,6 +146,7 @@ pub fn build() -> App<'static, 'static> {
                 .long("timesort")
                 .overrides_with("sizesort")
                 .overrides_with("extensionsort")
+                .overrides_with("versionsort")
                 .multiple(true)
                 .help("Sort by time modified"),
         )
@@ -155,6 +156,7 @@ pub fn build() -> App<'static, 'static> {
                 .long("sizesort")
                 .overrides_with("timesort")
                 .overrides_with("extensionsort")
+                .overrides_with("versionsort")
                 .multiple(true)
                 .help("Sort by size"),
         )
@@ -164,9 +166,19 @@ pub fn build() -> App<'static, 'static> {
                 .long("extensionsort")
                 .overrides_with("sizesort")
                 .overrides_with("timesort")
+                .overrides_with("versionsort")
                 .multiple(true)
                 .help("Sort by file extension"),
         )
+        .arg(
+            Arg::with_name("versionsort")
+                .short("v")
+                .multiple(true)
+                .overrides_with("timesort")
+                .overrides_with("sizesort")
+                .overrides_with("extensionsort")
+                .help("Natural sort of (version) numbers within text"),
+        )
         .arg(
             Arg::with_name("reverse")
                 .short("r")
diff --git a/src/flags.rs b/src/flags.rs
index 473b59077..fc430b2ce 100644
--- a/src/flags.rs
+++ b/src/flags.rs
@@ -60,6 +60,8 @@ impl Flags {
             SortFlag::Size
         } else if matches.is_present("extensionsort") {
             SortFlag::Extension
+        } else if matches.is_present("versionsort") {
+            SortFlag::Version
         } else {
             SortFlag::Name
         };
@@ -308,6 +310,7 @@ pub enum SortFlag {
     Name,
     Time,
     Size,
+    Version,
     Extension,
 }
 
diff --git a/src/meta/filetype.rs b/src/meta/filetype.rs
index 909480308..e7c275f8c 100644
--- a/src/meta/filetype.rs
+++ b/src/meta/filetype.rs
@@ -81,10 +81,7 @@ impl FileType {
     }
 
     pub fn is_dirlike(self) -> bool {
-        match self {
-            FileType::Directory { .. } | FileType::SymLink { is_dir: true } => true,
-            _ => false,
-        }
+        matches!(self, FileType::Directory { .. } | FileType::SymLink { is_dir: true })
     }
 }
 
diff --git a/src/sort.rs b/src/sort.rs
index ea5defa43..ab72929ce 100644
--- a/src/sort.rs
+++ b/src/sort.rs
@@ -1,5 +1,6 @@
 use crate::flags::{DirOrderFlag, Flags, SortFlag, SortOrder};
 use crate::meta::Meta;
+use human_sort::compare;
 use std::cmp::Ordering;
 
 pub type SortFn = fn(&Meta, &Meta) -> Ordering;
@@ -19,6 +20,7 @@ pub fn assemble_sorters(flags: &Flags) -> Vec<(SortOrder, SortFn)> {
         SortFlag::Name => by_name,
         SortFlag::Size => by_size,
         SortFlag::Time => by_date,
+        SortFlag::Version => by_version,
         SortFlag::Extension => by_extension,
     };
     sorters.push((flags.sort_order, other_sort));
@@ -56,6 +58,10 @@ fn by_date(a: &Meta, b: &Meta) -> Ordering {
     b.date.cmp(&a.date).then(a.name.cmp(&b.name))
 }
 
+fn by_version(a: &Meta, b: &Meta) -> Ordering {
+    compare(&a.name.name, &b.name.name)
+}
+
 fn by_extension(a: &Meta, b: &Meta) -> Ordering {
     a.name.extension().cmp(&b.name.extension())
 }
@@ -263,4 +269,30 @@ mod tests {
         let sorter = assemble_sorters(&flags);
         assert_eq!(by_meta(&sorter, &meta_a, &meta_t), Ordering::Less);
     }
+
+    #[test]
+    fn test_sort_assemble_sorters_by_version() {
+        let tmp_dir = tempdir().expect("failed to create temp dir");
+
+        let path_a = tmp_dir.path().join("2");
+        File::create(&path_a).expect("failed to create file");
+        let meta_a = Meta::from_path(&path_a, false).expect("failed to get meta");
+
+        let path_b = tmp_dir.path().join("11");
+        File::create(&path_b).expect("failed to create file");
+        let meta_b = Meta::from_path(&path_b, false).expect("failed to get meta");
+
+        let path_c = tmp_dir.path().join("12");
+        File::create(&path_c).expect("failed to create file");
+        let meta_c = Meta::from_path(&path_c, false).expect("failed to get meta");
+
+        let mut flags = Flags::default();
+        flags.sort_by = SortFlag::Version;
+
+        let sorter = assemble_sorters(&flags);
+        assert_eq!(by_meta(&sorter, &meta_b, &meta_a), Ordering::Greater);
+
+        let sorter = assemble_sorters(&flags);
+        assert_eq!(by_meta(&sorter, &meta_b, &meta_c), Ordering::Less);
+    }
 }


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index 554c72b6d..4fb81e811 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -260,6 +260,53 @@ fn test_show_folder_of_symlink_for_long_multi() {
         .stdout(predicate::str::contains("link/:"));
 }
 
+#[test]
+fn test_version_sort() {
+    let dir = tempdir();
+    dir.child("0.3.7").touch().unwrap();
+    dir.child("0.11.5").touch().unwrap();
+    dir.child("11a").touch().unwrap();
+    dir.child("0.2").touch().unwrap();
+    dir.child("0.11").touch().unwrap();
+    dir.child("1").touch().unwrap();
+    dir.child("11").touch().unwrap();
+    dir.child("2").touch().unwrap();
+    dir.child("22").touch().unwrap();
+    cmd().arg("-v").arg(dir.path()).assert().stdout(
+        predicate::str::is_match("0.2\n0.3.7\n0.11\n0.11.5\n1\n2\n11\n11a\n22\n$").unwrap(),
+    );
+}
+
+#[test]
+fn test_version_sort_overwrite_by_timesort() {
+    let dir = tempdir();
+    dir.child("2").touch().unwrap();
+    dir.child("11").touch().unwrap();
+    cmd()
+        .arg("-v")
+        .arg("-t")
+        .arg(dir.path())
+        .assert()
+        .stdout(predicate::str::is_match("11\n2\n$").unwrap());
+}
+
+#[test]
+fn test_version_sort_overwrite_by_sizesort() {
+    use std::fs::File;
+    use std::io::Write;
+    let dir = tempdir();
+    dir.child("2").touch().unwrap();
+    let larger = dir.path().join("11");
+    let mut larger_file = File::create(larger).unwrap();
+    writeln!(larger_file, "this is larger").unwrap();
+    cmd()
+        .arg("-v")
+        .arg("-S")
+        .arg(dir.path())
+        .assert()
+        .stdout(predicate::str::is_match("11\n2\n$").unwrap());
+}
+
 fn cmd() -> Command {
     Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap()
 }