diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index cfb01e71fa..9d029e3a6f 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -497,25 +497,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let date = if is_empty_or_whitespace || input == "-" || is_military_j { // Treat empty string, single hyphen, or 'J' as midnight today in local time - let date_part = - strtime::format("%F", &now).unwrap_or_else(|_| String::from("1970-01-01")); - let offset = if settings.utc { - String::from("+00:00") - } else { - strtime::format("%:z", &now).unwrap_or_default() - }; - let composed = if offset.is_empty() { - format!("{date_part} 00:00") - } else { - format!("{date_part} 00:00 {offset}") - }; if settings.debug { let _ = writeln!( stderr(), "date: warning: using midnight as starting time: 00:00:00" ); } - parse(&composed, false) + parse(&midnight_today(&now, settings.utc), false) } else if let Some((total_hours, day_delta)) = military_tz_with_offset { // Military timezone with optional hour offset // Convert to UTC time: midnight + military_tz_offset + additional_hours @@ -576,6 +564,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { DateSource::Stdin => parse_dates_from_reader( std::io::stdin(), &now, + settings.utc, DebugOptions::new(settings.debug, true), allow_extended, ), @@ -590,6 +579,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { parse_dates_from_reader( file, &now, + settings.utc, DebugOptions::new(settings.debug, true), allow_extended, ) @@ -1144,6 +1134,30 @@ fn try_parse_with_abbreviation>(date_str: S, now: &Zoned) -> Optio Some(zoned.with_time_zone(now.time_zone().clone())) } +/// Whether a date string only asks for midnight today: it is empty, whitespace +/// (possibly a parenthesized comment), or a lone `-`. +fn is_midnight_today_input(input: &str) -> bool { + let input = strip_parenthesized_comments(input); + let input = input.trim(); + input.is_empty() || input == "-" +} + +/// The date string for midnight today, in the local time zone or in UTC. +/// +/// GNU parses an empty date string (or a lone `-`) as midnight today; this is +/// the equivalent input for our parser. The start of the day is resolved in +/// the time zone itself, because on a DST transition day the UTC offset at +/// midnight is not the offset right now. +fn midnight_today(now: &Zoned, utc: bool) -> String { + let today = if utc { + now.with_time_zone(TimeZone::UTC) + } else { + now.clone() + }; + let midnight = today.start_of_day().unwrap_or(today); + strtime::format("%F %H:%M %:z", &midnight).unwrap_or_else(|_| String::from("1970-01-01 00:00")) +} + /// Helper function to parse dates from a line-based reader (stdin or file) /// /// Takes any `Read` source, reads it line by line, and parses each line as a date. @@ -1151,11 +1165,13 @@ fn try_parse_with_abbreviation>(date_str: S, now: &Zoned) -> Optio fn parse_dates_from_reader( reader: R, now: &Zoned, + utc: bool, dbg_opts: DebugOptions, allow_extended: bool, ) -> Box< dyn Iterator> + '_, > { + let midnight = midnight_today(now, utc); let lines = BufReader::new(reader).split(b'\n'); Box::new(lines.map_while(Result::ok).map(move |mut bytes| { // Strip a trailing '\r' (CRLF input; GNU's lexer ignores it too) @@ -1163,6 +1179,22 @@ fn parse_dates_from_reader( bytes.pop(); } match String::from_utf8(bytes) { + // GNU compatibility: an empty (or whitespace-only) line and a lone + // hyphen are midnight today, just like `-d ''`, not the current time. + Ok(s) if is_midnight_today_input(&s) => { + if dbg_opts.debug { + let _ = writeln!( + stderr(), + "date: warning: using midnight as starting time: 00:00:00" + ); + } + parse_date( + &midnight, + now, + DebugOptions::new(dbg_opts.debug, false), + allow_extended, + ) + } Ok(s) => parse_date(s, now, dbg_opts, allow_extended), // Report lines with invalid UTF-8 (with non-printable bytes // octal-escaped like GNU) instead of silently stopping the input diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 0e0f77c026..94cf9d766b 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -682,6 +682,34 @@ fn test_date_stdin_invalid_utf8_line() { .stderr_contains("date: invalid date 'Hello\\377x'"); } +#[test] +fn test_date_file_blank_line_is_midnight() { + // GNU treats an empty (or whitespace-only) line and a lone `-` the same + // way as `-d ''`: midnight today, not the current time. + new_ucmd!() + .env("TZ", "UTC0") + .args(&["-f", "-", "+%T"]) + .pipe_in("\n \n-\n2023-03-27 08:30:00\n\r\n") + .succeeds() + .stdout_is("00:00:00\n00:00:00\n00:00:00\n08:30:00\n00:00:00\n"); +} + +#[test] +fn test_date_file_blank_line_is_midnight_in_local_time() { + new_ucmd!() + .env("TZ", "Asia/Tokyo") + .args(&["-f", "-", "+%T %Z"]) + .pipe_in("\n") + .succeeds() + .stdout_is("00:00:00 JST\n"); + new_ucmd!() + .env("TZ", "Asia/Tokyo") + .args(&["-u", "-f", "-", "+%T %Z"]) + .pipe_in("\n") + .succeeds() + .stdout_is("00:00:00 UTC\n"); +} + #[test] fn test_date_for_file_mtime() { let (at, mut ucmd) = at_and_ucmd!();