diff --git a/bin/date/date.1 b/bin/date/date.1 --- a/bin/date/date.1 +++ b/bin/date/date.1 @@ -29,7 +29,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd November 5, 2025 +.Dd November 10, 2025 .Dt DATE 1 .Os .Sh NAME @@ -186,7 +186,7 @@ .Ar seconds , where .Ar seconds -is the number of seconds since the Epoch +is the number of seconds since the Unix Epoch (00:00:00 UTC, January 1, 1970; see .Xr time 3 ) , @@ -321,20 +321,43 @@ .Pp An operand with a leading plus .Pq Sq + -sign signals a user-defined format string +sign specifies a user-defined format string which specifies the format in which to display the date and time. The format string may contain any of the conversion specifications described in the .Xr strftime 3 -manual page and -.Ql \&%N -for nanoseconds, as well as any arbitrary text. +manual page, as well as any arbitrary text. +.Pp +The following extensions to the regular +.Xr strftime 3 +syntax are supported: +.Bl -tag -width "xxxx" +.It Cm \&% Ns Ar n Ns Cm N +Replaced by the +.Ar n Ns +-digit fractional part of the number of seconds since the Unix Epoch. +If +.Ar n +is omitted or zero, a default value of 9 is used, resulting in a +number with nanosecond resolution (hence the choice of the letter +.Sq N +for this conversion). +Note that the underlying clock may not necessarily support nanosecond +resolution. +.It Cm \&%-N +As above, but automatically choose the precision based on the reported +resolution of the underlying clock. +If the +.Fl r +option was specified, the default precision of 9 digits is used. +.El +.Pp A newline .Pq Ql \en character is always output after the characters specified by the format string. The format string for the default display is -.Dq +%+ . +.Dq %+ . .Pp If an operand does not have a leading plus sign, it is interpreted as a value for setting the system's notion of the current date and time. @@ -448,6 +471,13 @@ utility exits 0 on success, 1 if unable to set the date, and 2 if able to set the local date, but unable to set it globally. .Sh EXAMPLES +The command +.Pp +.Dl "date +%s.%3N" +.Pp +will print the time elapsed since the Unix Epoch with millisecond +precision. +.Pp The command: .Pp .Dl "date ""+DATE: %Y-%m-%d%nTIME: %H:%M:%S""" @@ -619,3 +649,9 @@ .Ql \&%N conversion specification was added in .Fx 14.1 . +Support for the +.Ql \&% Ns Ar n Ns Cm N +and +.Ql \&%-N +variants was added in +.Fx 15.1 . diff --git a/bin/date/date.c b/bin/date/date.c --- a/bin/date/date.c +++ b/bin/date/date.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -55,10 +56,10 @@ static void iso8601_usage(const char *) __dead2; static void multipleformats(void); static void printdate(const char *); -static void printisodate(struct tm *, long); +static void printisodate(struct tm *, long, long); static void setthetime(const char *, const char *, int, struct timespec *); static size_t strftime_ns(char * __restrict, size_t, const char * __restrict, - const struct tm * __restrict, long); + const struct tm * __restrict, long, long); static void usage(void) __dead2; static const struct iso8601_fmt { @@ -78,26 +79,24 @@ int main(int argc, char *argv[]) { - struct timespec ts; + struct timespec ts = { 0, 0 }, tres = { 0, 1 }; int ch, rflag; bool Iflag, jflag, Rflag; const char *format; char buf[1024]; - char *fmt, *outzone = NULL; - char *tmp; + char *end, *fmt, *outzone = NULL; struct vary *v; const struct vary *badv; struct tm *lt; struct stat sb; size_t i; + intmax_t number; v = NULL; fmt = NULL; (void) setlocale(LC_TIME, ""); rflag = 0; Iflag = jflag = Rflag = 0; - ts.tv_sec = 0; - ts.tv_nsec = 0; while ((ch = getopt(argc, argv, "f:I::jnRr:uv:z:")) != -1) switch((char)ch) { case 'f': @@ -131,13 +130,15 @@ break; case 'r': /* user specified seconds */ rflag = 1; - ts.tv_sec = strtoq(optarg, &tmp, 0); - if (*tmp != 0) { - if (stat(optarg, &sb) == 0) { - ts.tv_sec = sb.st_mtim.tv_sec; - ts.tv_nsec = sb.st_mtim.tv_nsec; - } else - usage(); + number = strtoimax(optarg, &end, 0); + if (end > optarg && *end == '\0') { + ts.tv_sec = number; + ts.tv_nsec = 0; + } else if (stat(optarg, &sb) == 0) { + ts.tv_sec = sb.st_mtim.tv_sec; + ts.tv_nsec = sb.st_mtim.tv_nsec; + } else { + usage(); } break; case 'u': /* do everything in UTC */ @@ -155,8 +156,12 @@ argc -= optind; argv += optind; - if (!rflag && clock_gettime(CLOCK_REALTIME, &ts) == -1) - err(1, "clock_gettime"); + if (!rflag) { + if (clock_gettime(CLOCK_REALTIME, &ts) == -1) + err(1, "clock_gettime"); + if (clock_getres(CLOCK_REALTIME, &tres) == -1) + err(1, "clock_getres"); + } format = "%+"; @@ -191,14 +196,14 @@ badv = vary_apply(v, lt); if (badv) { fprintf(stderr, "%s: Cannot apply date adjustment\n", - badv->arg); + badv->arg); vary_destroy(v); usage(); } vary_destroy(v); if (Iflag) - printisodate(lt, ts.tv_nsec); + printisodate(lt, ts.tv_nsec, tres.tv_nsec); if (format == rfc2822_format) /* @@ -208,7 +213,8 @@ setlocale(LC_TIME, "C"); - (void)strftime_ns(buf, sizeof(buf), format, lt, ts.tv_nsec); + (void)strftime_ns(buf, sizeof(buf), format, lt, + ts.tv_nsec, tres.tv_nsec); printdate(buf); } @@ -222,7 +228,7 @@ } static void -printisodate(struct tm *lt, long nsec) +printisodate(struct tm *lt, long nsec, long res) { const struct iso8601_fmt *it; char fmtbuf[64], buf[64], tzbuf[8]; @@ -231,10 +237,10 @@ for (it = iso8601_fmts; it <= iso8601_selected; it++) strlcat(fmtbuf, it->format_string, sizeof(fmtbuf)); - (void)strftime_ns(buf, sizeof(buf), fmtbuf, lt, nsec); + (void)strftime_ns(buf, sizeof(buf), fmtbuf, lt, nsec, res); if (iso8601_selected > iso8601_fmts) { - (void)strftime_ns(tzbuf, sizeof(tzbuf), "%z", lt, nsec); + (void)strftime_ns(tzbuf, sizeof(tzbuf), "%z", lt, nsec, res); memmove(&tzbuf[4], &tzbuf[3], 3); tzbuf[3] = ':'; strlcat(buf, tzbuf, sizeof(buf)); @@ -370,16 +376,17 @@ */ static size_t strftime_ns(char * __restrict s, size_t maxsize, const char * __restrict format, - const struct tm * __restrict t, long nsec) + const struct tm * __restrict t, long nsec, long res) { - size_t prefixlen; size_t ret; char *newformat; char *oldformat; const char *prefix; const char *suffix; const char *tok; - bool seen_percent; + long number; + int i, len, prefixlen, width, zeroes; + bool seen_percent, seen_dash, seen_width; seen_percent = false; if ((newformat = strdup(format)) == NULL) @@ -392,36 +399,85 @@ * If the previous token was a percent sign, * then there are two percent tokens in a row. */ - if (seen_percent) + if (seen_percent) { seen_percent = false; - else + } else { seen_percent = true; + seen_dash = seen_width = false; + prefixlen = tok - newformat; + width = 0; + } break; case 'N': - if (seen_percent) { - oldformat = newformat; - prefix = oldformat; - prefixlen = tok - oldformat - 1; - suffix = tok + 1; + if (!seen_percent) + break; + oldformat = newformat; + prefix = oldformat; + suffix = tok + 1; + /* + * Prepare the number we are about to print. If + * the requested width is less than 9, we need to + * cut off the least significant digits. If it is + * more than 9, we will have to append zeroes. + */ + if (seen_dash) { /* - * Construct a new format string from the - * prefix (i.e., the part of the old format - * from its beginning to the currently handled - * "%N" conversion specification), the - * nanoseconds, and the suffix (i.e., the part - * of the old format from the next token to the - * end). + * Calculate number of singificant digits + * based on res which is the clock's + * resolution in nanoseconds. */ - if (asprintf(&newformat, "%.*s%.9ld%s", - (int)prefixlen, prefix, nsec, - suffix) < 0) { - err(1, "asprintf"); - } - free(oldformat); - tok = newformat + prefixlen + 9; + for (width = 9, number = res; + width > 0 && number > 0; + width--, number /= 10) + /* nothing */; + } + number = nsec; + zeroes = 0; + if (width == 0) { + width = 9; + } else if (width > 9) { + zeroes = width - 9; + width = 9; + } else { + for (i = 0; i < 9 - width; i++) + number /= 10; } + /* + * Construct a new format string from the prefix + * (i.e., the part of the old format from its + * beginning to the currently handled "%N" + * conversion specification), the nanoseconds, and + * the suffix (i.e., the part of the old format + * from the next token to the end). + */ + asprintf(&newformat, "%.*s%.*ld%.*d%n%s", prefixlen, + prefix, width, number, zeroes, 0, &len, suffix); + if (newformat == NULL) + err(1, "asprintf"); + free(oldformat); + tok = newformat + len - 1; seen_percent = false; break; + case '-': + if (seen_percent) { + if (seen_dash || seen_width) { + seen_percent = false; + break; + } + seen_dash = true; + } + break; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if (seen_percent) { + if (seen_dash) { + seen_percent = false; + break; + } + width = width * 10 + *tok - '0'; + seen_width = true; + } + break; default: seen_percent = false; break; diff --git a/bin/date/tests/format_string_test.sh b/bin/date/tests/format_string_test.sh --- a/bin/date/tests/format_string_test.sh +++ b/bin/date/tests/format_string_test.sh @@ -132,6 +132,8 @@ format_string_test M M 04 20 format_string_test m m 02 11 format_string_test N N 000000000 000000000 + format_string_test 3N 3N 000 000 + format_string_test 12N 12N 000000000000 000000000000 format_string_test p p AM PM format_string_test R R 07:04 21:20 format_string_test r r "07:04:03 AM" "09:20:00 PM"