diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 5baa75432..b63b04bf4 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -8,7 +8,7 @@ mod locale; use clap::{Arg, ArgAction, Command}; -use jiff::fmt::strtime; +use jiff::fmt::strtime::{self, BrokenDownTime, Config, PosixCustom}; use jiff::tz::{TimeZone, TimeZoneDatabase}; use jiff::{Timestamp, Zoned}; use std::collections::HashMap; @@ -431,21 +431,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = BufWriter::new(std::io::stdout().lock()); // Format all the dates + let config = Config::new().custom(PosixCustom::new()).lenient(true); for date in dates { match date { - // TODO: Switch to lenient formatting. - Ok(date) => match strtime::format(format_string, &date) { - Ok(s) => writeln!(stdout, "{s}").map_err(|e| { - USimpleError::new(1, translate!("date-error-write", "error" => e)) - })?, - Err(e) => { - let _ = stdout.flush(); - return Err(USimpleError::new( - 1, - translate!("date-error-invalid-format", "format" => format_string, "error" => e), - )); + Ok(date) => { + match BrokenDownTime::from(&date).to_string_with_config(&config, format_string) { + Ok(s) => writeln!(stdout, "{s}").map_err(|e| { + USimpleError::new(1, translate!("date-error-write", "error" => e)) + })?, + Err(e) => { + let _ = stdout.flush(); + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); + } } - }, + } Err((input, _err)) => { let _ = stdout.flush(); show!(USimpleError::new( diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 97b3d1056..744cffecb 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -480,9 +480,8 @@ fn test_date_set_valid_4() { #[test] fn test_invalid_format_string() { - let result = new_ucmd!().arg("+%!").fails(); - result.no_stdout(); - assert!(result.stderr_str().starts_with("date: invalid format ")); + // With lenient mode, invalid format sequences are output literally (like GNU date) + new_ucmd!().arg("+%!").succeeds().stdout_is("%!\n"); } #[test] @@ -1446,3 +1445,27 @@ fn test_date_locale_fr_french() { "Output should include timezone information, got: {stdout}" ); } + +#[test] +fn test_date_posix_format_specifiers() { + let cases = [ + // %r: 12-hour time with zero-padded hour (08:17:48 AM, not 8:17:48 AM) + ("%r", "08:17:48 AM"), + // %x: locale date in MM/DD/YY format + ("%x", "01/19/97"), + // %X: locale time in HH:MM:SS format + ("%X", "08:17:48"), + // %:8z: invalid format (width between : and z) should output literally (lenient mode) + ("%:8z", "%:8z"), + ]; + + for (format, expected) in cases { + new_ucmd!() + .env("TZ", "UTC") + .arg("-d") + .arg("1997-01-19 08:17:48") + .arg(format!("+{format}")) + .succeeds() + .stdout_is(format!("{expected}\n")); + } +}