Repository: uutils/coreutils (Rust)

Issue:
ls: implement extension sorting
There should be an option to sort the files alphabetically by extension using `-X` or `--sort=extension`. Files with the same extension should be sorted alphabetically by their filename.

The other sort options can be used as an example for the implementation. This option should be overrideable. For example, `ls -XS` should sort by file size, but `ls -SX` should sort by extension.


Discussion:

Patch:
diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs
index 0351227eb95..3cd1e037d7b 100644
--- a/src/uu/ls/src/ls.rs
+++ b/src/uu/ls/src/ls.rs
@@ -74,6 +74,7 @@ pub mod options {
         pub static TIME: &str = "t";
         pub static NONE: &str = "U";
         pub static VERSION: &str = "v";
+        pub static EXTENSION: &str = "X";
     }
     pub mod time {
         pub static ACCESS: &str = "u";
@@ -134,6 +135,7 @@ enum Sort {
     Size,
     Time,
     Version,
+    Extension,
 }
 
 enum SizeFormat {
@@ -277,6 +279,7 @@ impl Config {
                 "time" => Sort::Time,
                 "size" => Sort::Size,
                 "version" => Sort::Version,
+                "extension" => Sort::Extension,
                 // below should never happen as clap already restricts the values.
                 _ => unreachable!("Invalid field for --sort"),
             }
@@ -288,6 +291,8 @@ impl Config {
             Sort::None
         } else if options.is_present(options::sort::VERSION) {
             Sort::Version
+        } else if options.is_present(options::sort::EXTENSION) {
+            Sort::Extension
         } else {
             Sort::Name
         };
@@ -752,10 +757,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
         .arg(
             Arg::with_name(options::SORT)
                 .long(options::SORT)
-                .help("Sort by <field>: name, none (-U), time (-t) or size (-S)")
+                .help("Sort by <field>: name, none (-U), time (-t), size (-S) or extension (-X)")
                 .value_name("field")
                 .takes_value(true)
-                .possible_values(&["name", "none", "time", "size", "version"])
+                .possible_values(&["name", "none", "time", "size", "version", "extension"])
                 .require_equals(true)
                 .overrides_with_all(&[
                     options::SORT,
@@ -763,6 +768,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
                     options::sort::TIME,
                     options::sort::NONE,
                     options::sort::VERSION,
+                    options::sort::EXTENSION,
                 ])
         )
         .arg(
@@ -775,6 +781,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
                     options::sort::TIME,
                     options::sort::NONE,
                     options::sort::VERSION,
+                    options::sort::EXTENSION,
                 ])
         )
         .arg(
@@ -787,6 +794,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
                     options::sort::TIME,
                     options::sort::NONE,
                     options::sort::VERSION,
+                    options::sort::EXTENSION,
                 ])
         )
         .arg(
@@ -799,6 +807,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
                     options::sort::TIME,
                     options::sort::NONE,
                     options::sort::VERSION,
+                    options::sort::EXTENSION,
+                ])
+        )
+        .arg(
+            Arg::with_name(options::sort::EXTENSION)
+                .short(options::sort::EXTENSION)
+                .help("Sort alphabetically by entry extension.")
+                .overrides_with_all(&[
+                    options::SORT,
+                    options::sort::SIZE,
+                    options::sort::TIME,
+                    options::sort::NONE,
+                    options::sort::VERSION,
+                    options::sort::EXTENSION,
                 ])
         )
         .arg(
@@ -813,6 +835,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
                     options::sort::TIME,
                     options::sort::NONE,
                     options::sort::VERSION,
+                    options::sort::EXTENSION,
                 ])
         )
 
@@ -1141,6 +1164,12 @@ fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
         // The default sort in GNU ls is case insensitive
         Sort::Name => entries.sort_by_cached_key(|k| k.file_name.to_lowercase()),
         Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
+        Sort::Extension => entries.sort_by(|a, b| {
+            a.p_buf
+                .extension()
+                .cmp(&b.p_buf.extension())
+                .then(a.p_buf.file_stem().cmp(&b.p_buf.file_stem()))
+        }),
         Sort::None => {}
     }
 


Test patch:
diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs
index 09e02f2640b..b819ef159c3 100644
--- a/tests/by-util/test_ls.rs
+++ b/tests/by-util/test_ls.rs
@@ -1681,3 +1681,53 @@ fn test_ls_deref_command_line_dir() {
 
     assert!(!result.stdout_str().ends_with("sym_dir"));
 }
+
+#[test]
+fn test_ls_sort_extension() {
+    let scene = TestScenario::new(util_name!());
+    let at = &scene.fixtures;
+    for filename in &[
+        "file1",
+        "file2",
+        "anotherFile",
+        ".hidden",
+        ".file.1",
+        ".file.2",
+        "file.1",
+        "file.2",
+        "anotherFile.1",
+        "anotherFile.2",
+        "file.ext",
+        "file.debug",
+        "anotherFile.ext",
+        "anotherFile.debug",
+    ] {
+        at.touch(filename);
+    }
+
+    let expected = vec![
+        ".",
+        "..",
+        ".hidden",    
+        "anotherFile",
+        "file1",
+        "file2",
+        ".file.1",
+        "anotherFile.1",
+        "file.1",
+        ".file.2",
+        "anotherFile.2",
+        "file.2",
+        "anotherFile.debug",
+        "file.debug",
+        "anotherFile.ext",
+        "file.ext",
+        "", // because of '\n' at the end of the output
+    ];
+
+    let result = scene.ucmd().arg("-1aX").run();
+    assert_eq!(result.stdout_str().split('\n').collect::<Vec<_>>(), expected,);
+
+    let result = scene.ucmd().arg("-1a").arg("--sort=extension").run();
+    assert_eq!(result.stdout_str().split('\n').collect::<Vec<_>>(), expected,);
+}