diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index 8ad3a6ec3e6..72113c40567 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -82,6 +82,8 @@ date-help-iso-8601 = output date/time in ISO 8601 format. 'hours', 'minutes', 'seconds', or 'ns' for date and time to the indicated precision. Example: 2006-08-14T02:34:56-06:00 +date-help-resolution = output the available resolution of timestamps + Example: 0.000000001 date-help-rfc-email = output date and time in RFC 5322 format. Example: Mon, 14 Aug 2006 02:34:56 -0600 date-help-rfc-3339 = output date/time in RFC 3339 format. diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 6daf89f33f1..c3be8ba9df7 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -3,14 +3,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes +// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres use clap::{Arg, ArgAction, Command}; use jiff::fmt::strtime; use jiff::tz::TimeZone; use jiff::{Timestamp, Zoned}; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] -use libc::{CLOCK_REALTIME, clock_settime, timespec}; +use libc::clock_settime; +#[cfg(all(unix, not(target_os = "redox")))] +use libc::{CLOCK_REALTIME, clock_getres, timespec}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -35,6 +37,7 @@ const OPT_FORMAT: &str = "format"; const OPT_FILE: &str = "file"; const OPT_DEBUG: &str = "debug"; const OPT_ISO_8601: &str = "iso-8601"; +const OPT_RESOLUTION: &str = "resolution"; const OPT_RFC_EMAIL: &str = "rfc-email"; const OPT_RFC_822: &str = "rfc-822"; const OPT_RFC_2822: &str = "rfc-2822"; @@ -57,6 +60,7 @@ enum Format { Iso8601(Iso8601Format), Rfc5322, Rfc3339(Rfc3339Format), + Resolution, Custom(String), Default, } @@ -68,6 +72,7 @@ enum DateSource { FileMtime(PathBuf), Stdin, Human(String), + Resolution, } enum Iso8601Format { @@ -136,6 +141,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|s| s.as_str().into()) { Format::Rfc3339(fmt) + } else if matches.get_flag(OPT_RESOLUTION) { + Format::Resolution } else { Format::Default }; @@ -149,6 +156,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } else if let Some(file) = matches.get_one::(OPT_REFERENCE) { DateSource::FileMtime(file.into()) + } else if matches.get_flag(OPT_RESOLUTION) { + DateSource::Resolution } else { DateSource::Now }; @@ -230,6 +239,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let iter = std::iter::once(Ok(date)); Box::new(iter) } + DateSource::Resolution => { + let resolution = get_clock_resolution(); + let date = resolution.to_zoned(TimeZone::system()); + let iter = std::iter::once(Ok(date)); + Box::new(iter) + } DateSource::Now => { let iter = std::iter::once(Ok(now)); Box::new(iter) @@ -283,6 +298,7 @@ pub fn uu_app() -> Command { .long(OPT_FILE) .value_name("DATEFILE") .value_hint(clap::ValueHint::FilePath) + .conflicts_with(OPT_DATE) .help(translate!("date-help-file")), ) .arg( @@ -297,6 +313,14 @@ pub fn uu_app() -> Command { .default_missing_value(OPT_DATE) .help(translate!("date-help-iso-8601")), ) + .arg( + Arg::new(OPT_RESOLUTION) + .long(OPT_RESOLUTION) + .conflicts_with_all([OPT_DATE, OPT_FILE]) + .overrides_with(OPT_RESOLUTION) + .help(translate!("date-help-resolution")) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(OPT_RFC_EMAIL) .short('R') @@ -325,6 +349,7 @@ pub fn uu_app() -> Command { .long(OPT_REFERENCE) .value_name("FILE") .value_hint(clap::ValueHint::AnyPath) + .conflicts_with_all([OPT_DATE, OPT_FILE, OPT_RESOLUTION]) .help(translate!("date-help-reference")), ) .arg( @@ -374,6 +399,7 @@ fn make_format_string(settings: &Settings) -> &str { Rfc3339Format::Seconds => "%F %T%:z", Rfc3339Format::Ns => "%F %T.%N%:z", }, + Format::Resolution => "%s.%N", Format::Custom(ref fmt) => fmt, Format::Default => "%a %b %e %X %Z %Y", } @@ -398,6 +424,50 @@ fn parse_date + Clone>( } } +#[cfg(not(any(unix, windows)))] +fn get_clock_resolution() -> Timestamp { + unimplemented!("getting clock resolution not implemented (unsupported target)"); +} + +#[cfg(all(unix, not(target_os = "redox")))] +fn get_clock_resolution() -> Timestamp { + let mut timespec = timespec { + tv_sec: 0, + tv_nsec: 0, + }; + unsafe { + // SAFETY: the timespec struct lives for the full duration of this function call. + // + // The clock_getres function can only fail if the passed clock_id is not + // a known clock. All compliant posix implementors must support + // CLOCK_REALTIME, therefore this function call cannot fail on any + // compliant posix implementation. + // + // See more here: + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/clock_getres.html + clock_getres(CLOCK_REALTIME, &raw mut timespec); + } + #[allow(clippy::unnecessary_cast)] // Cast required on 32-bit platforms + Timestamp::constant(timespec.tv_sec as i64, timespec.tv_nsec as i32) +} + +#[cfg(all(unix, target_os = "redox"))] +fn get_clock_resolution() -> Timestamp { + // Redox OS does not support the posix clock_getres function, however + // internally it uses a resolution of 1ns to represent timestamps. + // https://gitlab.redox-os.org/redox-os/kernel/-/blob/master/src/time.rs + Timestamp::constant(0, 1) +} + +#[cfg(windows)] +fn get_clock_resolution() -> Timestamp { + // Windows does not expose a system call for getting the resolution of the + // clock, however the FILETIME struct returned by GetSystemTimeAsFileTime, + // and GetSystemTimePreciseAsFileTime has a resolution of 100ns. + // https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime + Timestamp::constant(0, 100) +} + #[cfg(not(any(unix, windows)))] fn set_system_datetime(_date: Zoned) -> UResult<()> { unimplemented!("setting date not implemented (unsupported target)"); diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index f9b4caeba80..49514eb5866 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata +// spell-checker: ignore: AEDT AEST EEST NZDT NZST Kolkata Iseconds use chrono::{DateTime, Datelike, Duration, NaiveTime, Utc}; // spell-checker:disable-line use regex::Regex; @@ -745,3 +745,36 @@ fn test_date_empty_tz_time() { .succeeds() .stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); } + +#[test] +fn test_date_resolution() { + // Test that --resolution flag returns a floating point number by default + new_ucmd!() + .arg("--resolution") + .succeeds() + .stdout_str_check(|s| s.trim().parse::().is_ok()); + + // Test that --resolution flag can be passed twice to match gnu + new_ucmd!() + .arg("--resolution") + .arg("--resolution") + .succeeds() + .stdout_str_check(|s| s.trim().parse::().is_ok()); + + // Test that can --resolution output can be formatted as a date + new_ucmd!() + .arg("--resolution") + .arg("-Iseconds") + .succeeds() + .stdout_only("1970-01-01T00:00:00+00:00\n"); +} + +#[test] +fn test_date_resolution_no_combine() { + // Test that date fails when --resolution flag is passed with date flag + new_ucmd!() + .arg("--resolution") + .arg("-d") + .arg("2025-01-01") + .fails(); +}