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), } 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 { + 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::(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::(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 = 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() } 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()