Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/uu/date/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 72 additions & 2 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -57,6 +60,7 @@ enum Format {
Iso8601(Iso8601Format),
Rfc5322,
Rfc3339(Rfc3339Format),
Resolution,
Custom(String),
Default,
}
Expand All @@ -68,6 +72,7 @@ enum DateSource {
FileMtime(PathBuf),
Stdin,
Human(String),
Resolution,
}

enum Iso8601Format {
Expand Down Expand Up @@ -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
};
Expand All @@ -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
};
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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')
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
}
Expand All @@ -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)");
Expand Down
35 changes: 34 additions & 1 deletion tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Loading