Repository: uutils/coreutils (Rust)

Issue:
Implement `TIME_STYLE="+DATE" ls`
We already support TIME_STYLE as an environment variable but only with the 4 different values.
GNU allows the use of +date

For example:
```
$ touch exe
$ TIME_STYLE="+%Y__%S" ls exe -al
-rw-r--r-- 1 sylvestre sylvestre 0 2022__58 exe
$ TIME_STYLE="+%Y-%m-%d %H:%M:%S.%N %z" ls exe -al
-rw-r--r-- 1 sylvestre sylvestre 0 2022-06-13 21:34:58.728327312 +0200 exe
$ TIME_STYLE="+foobar" ls exe -al
-rw-r--r-- 1 sylvestre sylvestre 0 foobar exe

```

GNU supports the date format (like the date or touch commands)
https://github.com/uutils/coreutils/blob/main/src/uu/date/src/date.rs
https://github.com/uutils/coreutils/blob/main/src/uu/touch/src/touch.rs

Tests in:
https://github.com/coreutils/coreutils/blob/master/tests/ls/color-term.sh#L23=



Discussion:
Hi. I would also like to try this issue. I have made some progress, and technically it works except for the clap validation, see problem on a question I made to [clap-repo](https://github.com/clap-rs/clap/discussions/4260#discussioncomment-3736434).

Basically right now we have [value_parser!](https://github.com/uutils/coreutils/blob/main/src/uu/ls/src/ls.rs#L1518), but not supporting +FORMAT. Thus, by supporting +FORMAT, it cannot match the string anymore, but rather a user defined string with the `+` key in front, thus making it a bit more difficult, and as far as I am concerned, requiring a custom logic in case parse fails.

In my question to clap, the maintainer suggested something, and I tried some stuff but it looks so far a bit beyond my capabilities, so if anyone has any other ideas, lets discuss :) 
Hi! Sounds very promising! You could always remove the parsing from clap, accept any string and then manually parse the string value later. What argument is this about by the way? In the issue I only see an environment variable, which clap should not have anything to do with right?
Hi! Your initial suggestion seems like doable. The argument is [`--time-style`](https://github.com/uutils/coreutils/blob/b43bbe9e98d60b05cd98a02969d608cb1622f0d4/src/uu/ls/src/ls.rs#L1511-L1520) which accepts(for now)  `  .value_parser(["full-iso", "long-iso", "iso", "locale"])`. However if I add there `+FORMAT` then its no longer accepted. If I remove completely validation it works fine, but then you dont get the nice clap help message such as: `Possible values are: ["full-iso", "long-iso", "iso", "locale"]` etc. 
Yeah we indeed lose the message and we'd need some custom message instead, but I'm not sure that the message from clap would still be as nice with a custom value_parser.



Patch:
diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs
index fd9ae34dbd4..1be54adcea9 100644
--- a/src/uu/ls/src/ls.rs
+++ b/src/uu/ls/src/ls.rs
@@ -10,7 +10,10 @@
 #[macro_use]
 extern crate uucore;
 
-use clap::{builder::ValueParser, crate_version, Arg, Command};
+use clap::{
+    builder::{NonEmptyStringValueParser, ValueParser},
+    crate_version, Arg, Command,
+};
 use glob::Pattern;
 use lscolors::LsColors;
 use number_prefix::NumberPrefix;
@@ -156,6 +159,7 @@ enum LsError {
     IOErrorContext(std::io::Error, PathBuf, bool),
     BlockSizeParseError(String),
     AlreadyListedError(PathBuf),
+    TimeStyleParseError(String, Vec<String>),
 }
 
 impl UError for LsError {
@@ -167,6 +171,7 @@ impl UError for LsError {
             Self::IOErrorContext(_, _, true) => 2,
             Self::BlockSizeParseError(_) => 1,
             Self::AlreadyListedError(_) => 2,
+            Self::TimeStyleParseError(_, _) => 1,
         }
     }
 }
@@ -179,6 +184,14 @@ impl Display for LsError {
             Self::BlockSizeParseError(s) => {
                 write!(f, "invalid --block-size argument {}", s.quote())
             }
+            Self::TimeStyleParseError(s, possible_time_styles) => {
+                write!(
+                    f,
+                    "invalid --time-style argument {}\nPossible values are: {:?}\n\nFor more information try --help",
+                    s.quote(),
+                    possible_time_styles
+                )
+            }
             Self::InvalidLineWidth(s) => write!(f, "invalid line width: {}", s.quote()),
             Self::IOError(e) => write!(f, "general io error: {}", e),
             Self::IOErrorContext(e, p, _) => {
@@ -300,8 +313,46 @@ enum TimeStyle {
     LongIso,
     Iso,
     Locale,
+    Format(String),
 }
 
+fn parse_time_style(options: &clap::ArgMatches) -> Result<TimeStyle, LsError> {
+    let possible_time_styles = vec![
+        "full-iso".to_string(),
+        "long-iso".to_string(),
+        "iso".to_string(),
+        "locale".to_string(),
+        "+FORMAT (e.g., +%H:%M) for a 'date'-style format".to_string(),
+    ];
+    if let Some(field) = options.get_one::<String>(options::TIME_STYLE) {
+        //If both FULL_TIME and TIME_STYLE are present
+        //The one added last is dominant
+        if options.contains_id(options::FULL_TIME)
+            && options.indices_of(options::FULL_TIME).unwrap().last()
+                > options.indices_of(options::TIME_STYLE).unwrap().last()
+        {
+            Ok(TimeStyle::FullIso)
+        } else {
+            match field.as_str() {
+                "full-iso" => Ok(TimeStyle::FullIso),
+                "long-iso" => Ok(TimeStyle::LongIso),
+                "iso" => Ok(TimeStyle::Iso),
+                "locale" => Ok(TimeStyle::Locale),
+                _ => match field.chars().next().unwrap() {
+                    '+' => Ok(TimeStyle::Format(String::from(&field[1..]))),
+                    _ => Err(LsError::TimeStyleParseError(
+                        String::from(field),
+                        possible_time_styles,
+                    )),
+                },
+            }
+        }
+    } else if options.contains_id(options::FULL_TIME) {
+        Ok(TimeStyle::FullIso)
+    } else {
+        Ok(TimeStyle::Locale)
+    }
+}
 enum Dereference {
     None,
     DirArgs,
@@ -700,31 +751,7 @@ impl Config {
         } else {
             IndicatorStyle::None
         };
-
-        let time_style = if let Some(field) = options.get_one::<String>(options::TIME_STYLE) {
-            //If both FULL_TIME and TIME_STYLE are present
-            //The one added last is dominant
-            if options.contains_id(options::FULL_TIME)
-                && options.indices_of(options::FULL_TIME).unwrap().last()
-                    > options.indices_of(options::TIME_STYLE).unwrap().last()
-            {
-                TimeStyle::FullIso
-            } else {
-                //Clap handles the env variable "TIME_STYLE"
-                match field.as_str() {
-                    "full-iso" => TimeStyle::FullIso,
-                    "long-iso" => TimeStyle::LongIso,
-                    "iso" => TimeStyle::Iso,
-                    "locale" => TimeStyle::Locale,
-                    // below should never happen as clap already restricts the values.
-                    _ => unreachable!("Invalid field for --time-style"),
-                }
-            }
-        } else if options.contains_id(options::FULL_TIME) {
-            TimeStyle::FullIso
-        } else {
-            TimeStyle::Locale
-        };
+        let time_style = parse_time_style(options)?;
 
         let mut ignore_patterns: Vec<Pattern> = Vec::new();
 
@@ -1511,13 +1538,13 @@ pub fn uu_app<'a>() -> Command<'a> {
                 ]),
         )
         .arg(
-            //This still needs support for posix-*, +FORMAT
+            //This still needs support for posix-*
             Arg::new(options::TIME_STYLE)
                 .long(options::TIME_STYLE)
                 .help("time/date format with -l; see TIME_STYLE below")
                 .value_name("TIME_STYLE")
                 .env("TIME_STYLE")
-                .value_parser(["full-iso", "long-iso", "iso", "locale"])
+                .value_parser(NonEmptyStringValueParser::new())
                 .overrides_with_all(&[options::TIME_STYLE]),
         )
         .arg(
@@ -1549,7 +1576,7 @@ pub fn uu_app<'a>() -> Command<'a> {
                 .value_parser(ValueParser::os_string()),
         )
         .after_help(
-            "The TIME_STYLE argument can be full-iso, long-iso, iso. \
+            "The TIME_STYLE argument can be full-iso, long-iso, iso, locale or +FORMAT. FORMAT is interpreted like in date. \
             Also the TIME_STYLE environment variable sets the default style to use.",
         )
 }
@@ -2519,7 +2546,7 @@ fn display_date(metadata: &Metadata, config: &Config) -> String {
             //According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
             let recent = time + chrono::Duration::seconds(31_556_952 / 2) > chrono::Local::now();
 
-            match config.time_style {
+            match &config.time_style {
                 TimeStyle::FullIso => time.format("%Y-%m-%d %H:%M:%S.%f %z"),
                 TimeStyle::LongIso => time.format("%Y-%m-%d %H:%M"),
                 TimeStyle::Iso => time.format(if recent { "%m-%d %H:%M" } else { "%Y-%m-%d " }),
@@ -2534,6 +2561,7 @@ fn display_date(metadata: &Metadata, config: &Config) -> String {
 
                     time.format(fmt)
                 }
+                TimeStyle::Format(e) => time.format(e),
             }
             .to_string()
         }


Test patch:
diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs
index 79f3b07afae..e083464d64e 100644
--- a/tests/by-util/test_ls.rs
+++ b/tests/by-util/test_ls.rs
@@ -1653,6 +1653,7 @@ fn test_ls_styles() {
     let re_iso = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
     let re_locale =
         Regex::new(r"[a-z-]* \d* \w* \w* \d* [A-Z][a-z]{2} ( |\d)\d \d{2}:\d{2} test\n").unwrap();
+    let re_custom_format = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{4}__\d{2} test\n").unwrap();
 
     //full-iso
     let result = scene
@@ -1675,6 +1676,17 @@ fn test_ls_styles() {
     let result = scene.ucmd().arg("-l").arg("--time-style=locale").succeeds();
     assert!(re_locale.is_match(result.stdout_str()));
 
+    //+FORMAT
+    let result = scene
+        .ucmd()
+        .arg("-l")
+        .arg("--time-style=+%Y__%M")
+        .succeeds();
+    assert!(re_custom_format.is_match(result.stdout_str()));
+
+    // Also fails due to not having full clap support for time_styles
+    scene.ucmd().arg("-l").arg("-time-style=invalid").fails();
+
     //Overwrite options tests
     let result = scene
         .ucmd()