date: implement --resolution flag (#8686)

* date: implement --resolution flag

* date: add tests for --resolution flag

* date: add date source option conflicts

* fixup: fix type error on 32-bit platforms

* fixup: fix spelling issue in test

* fixup: use hardcoded resolution on redox os
This commit is contained in:
Andrew Twigg
2025-09-22 11:23:50 +02:00
committed by GitHub
parent e2f5dcb9bb
commit 542e4762f3
3 changed files with 108 additions and 3 deletions
+2
View File
@@ -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.
+72 -2
View File
@@ -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::<String>(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<S: AsRef<str> + 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)");
+34 -1
View File
@@ -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::<f64>().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::<f64>().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();
}