commit: f7f76feba067d99151125e39113366acd6857acd
parent 7b1db006810ec2183efe9c360fe9bf4e14adede9
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Mon, 10 Feb 2025 22:18:03 +0100
cmd/date: add support for -I option along with %N and %:z
Diffstat:
4 files changed, 169 insertions(+), 26 deletions(-)
diff --git a/cmd/date.1.in b/cmd/date.1.in
@@ -10,14 +10,17 @@
.Sh SYNOPSIS
.Nm
.Op Fl jRu
+.Op Fl I Ar iso_fmt
.Op Fl d Ar datetime | Fl r Ar epoch
.Op Cm + Ns Ar format
.Nm
.Op Fl jRu
+.Op Fl I Ar iso_fmt
.Ar mmddHHMM Ns Oo Oo Ar CC Oc Ns Ar yy Oc
.Op Cm + Ns Ar format
.Nm
.Op Fl jRu
+.Op Fl I Ar iso_fmt
.Fl f Ar now_format
.Ar now
.Op Cm + Ns Ar format
@@ -41,6 +44,22 @@ as the
format string for
.Ar now ,
which will be used instead of the current datetime.
+.It Fl I Ar iso_fmt
+Set the ISO-8601 resolution to format at with setting
+.Ar iso_fmt
+to one of the following values:
+.Bl -tag -width m_inutes_
+.It Ar d Ns Op Ar ate
+date, equivalent to +%Y-%m-%d
+.It Ar h Ns Op Ar ours
+hours, equivalent to +%Y-%m-%dT%H%:z
+.It Ar m Ns Op Ar inutes
+minutes, equivalent to +%Y-%m-%dT%H:%M%:z
+.It Ar s Ns Op Ar econds
+seconds, equivalent to +%Y-%m-%dT%H:%M:%S%:z
+.It Ar n Ns Op Ar s
+nano-seconds, equivalent to +%Y-%m-%dT%H:%M:%S,%N%:z
+.El
.It Fl j
Do no set the system date.
This allows to use the
@@ -83,7 +102,10 @@ For example 072505542024 corresponds to 2024-07-25T05:54, as you can verify with
.It Cm + Ns Ar format
Set the displayed datetime in
.Xr strftime 3
-format.
+format,
+with additionally
+%N for nanoseconds and %:z for colon-separated timezone (±ZZ:ZZ).
+.br
Otherwise defaults to
.Ql %c
.El
@@ -121,5 +143,7 @@ option is inspired from BSD and illumos,
and
.Fl j
options are inspired by FreeBSD and NetBSD.
+.Pp
+The %N and %:z formats are extensions inspired from GNU coreutils.
.Sh AUTHORS
.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me
diff --git a/cmd/date.c b/cmd/date.c
@@ -8,6 +8,7 @@
#include "../lib/iso_parse.h" /* iso_parse */
+#include <assert.h>
#include <errno.h>
#include <locale.h> /* setlocale() */
#include <stdbool.h>
@@ -19,14 +20,85 @@
const char *argv0 = "date";
+static size_t
+date_strftime(char *restrict buf,
+ size_t buflen,
+ const char *restrict fmt,
+ const struct tm *restrict tm,
+ long nsec)
+{
+ size_t fmtlen = strlen(fmt);
+ size_t printed = 0;
+
+ if(fmtlen == 0) return 0;
+
+ for(size_t i = 0; i < fmtlen;)
+ {
+ // size taken from musl strftime
+ static char fmt_buf[100] = "";
+ size_t fmt_bufi = 0;
+
+ if(fmt[i] == '%') fmt_buf[fmt_bufi++] = fmt[i++];
+
+ if(fmt[i] == '%') // handle '%%'
+ {
+ *buf = '%';
+ buf++;
+ buflen--;
+ printed++;
+ i++;
+ continue;
+ }
+
+ for(; fmt[i] != '%' && i < fmtlen;)
+ {
+ fmt_buf[fmt_bufi++] = fmt[i++];
+ assert(fmt_bufi < 100);
+ }
+
+ fmt_buf[fmt_bufi] = '\0';
+
+ if(fmt_buf[0] == '%' && fmt_buf[1] == ':' && fmt_buf[2] == 'z')
+ {
+ size_t got =
+ snprintf(buf, buflen, "%+.2ld:%.2ld", tm->tm_gmtoff / 3600, tm->tm_gmtoff % 3600 / 60);
+ if(got == 0) return got;
+
+ buf += got;
+ buflen -= got;
+ printed += got;
+ }
+ else if(fmt_buf[0] == '%' && fmt_buf[1] == 'N')
+ {
+ size_t got = snprintf(buf, buflen, "%09ld", nsec);
+ if(got == 0) return got;
+
+ buf += got;
+ buflen -= got;
+ printed += got;
+ }
+ else
+ {
+ size_t got = strftime(buf, buflen, fmt_buf, tm);
+ if(got == 0) return got;
+
+ buf += got;
+ buflen -= got;
+ printed += got;
+ }
+ }
+
+ return printed;
+}
+
static void
usage(void)
{
fprintf(stderr, "\
Usage:\n\
- date [-jRu] [-d datetime | -r epoch] [+format]\n\
- date [-jRu] mmddHHMM[[CC]yy] [+format]\n\
- date [-jRu] -f now_format now [+format]\n\
+ date [-jRu] [-I iso_fmt] [-d datetime | -r epoch] [+format]\n\
+ date [-jRu] [-I iso_fmt] mmddHHMM[[CC]yy] [+format]\n\
+ date [-jRu] [-I iso_fmt] -f now_format now [+format]\n\
");
}
@@ -65,7 +137,7 @@ main(int argc, char *argv[])
return 1;
}
- for(int c = -1; (c = getopt(argc, argv, ":d:f:jr:Ru")) != -1;)
+ for(int c = -1; (c = getopt(argc, argv, ":d:f:I:jr:Ru")) != -1;)
{
const char *errstr = NULL;
switch(c)
@@ -150,6 +222,28 @@ main(int argc, char *argv[])
setenv("TZ", "UTC", 1);
tzset();
break;
+ case 'I': /* ISO 8601 */
+ /* note: %:z (±ZZ:ZZ) and %N (nanoseconds) are date(1) GNU-isms absent from C libraries including glibc */
+ switch(optarg[0])
+ {
+ case 'h': // hours
+ format = "%Y-%m-%dT%H%:z";
+ break;
+ case 'm': // minutes
+ format = "%Y-%m-%dT%H:%M%:z";
+ break;
+ case 's': // seconds
+ format = "%Y-%m-%dT%H:%M:%S%:z";
+ break;
+ case 'n': // ns, nanoseconds
+ format = "%Y-%m-%dT%H:%M:%S,%N%:z";
+ break;
+ case 'd': // date
+ default:
+ format = "%Y-%m-%d";
+ break;
+ }
+ break;
case 'j':
jflag = true;
break;
@@ -287,7 +381,7 @@ main(int argc, char *argv[])
}
errno = 0;
- if(strftime(outstr, sizeof(outstr), format, &tm) == 0 && errno != 0)
+ if(date_strftime(outstr, sizeof(outstr), format, &tm, tp.tv_nsec) == 0 && errno != 0)
{
fprintf(stderr, "%s: error: Failed formatting time: %s\n", argv0, strerror(errno));
return 1;
diff --git a/lib/iso_parse.c b/lib/iso_parse.c
@@ -23,6 +23,8 @@
char *
iso_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
{
+ *nsec = 0;
+
// For Alpine's abuild compatibility
if(arg[0] == '@')
{
@@ -37,7 +39,6 @@ iso_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
return NULL;
}
- nsec = 0;
gmtime_r(&now, time);
return endptr;
@@ -109,10 +110,18 @@ iso_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
}
else
{
+#ifndef TZNAME_MAX
+#define TZNAME_MAX _POSIX_TZNAME_MAX
+#endif
+#if TZNAME_MAX < 5
+#error TZNAME_MAX is too small
+#endif
+ static char offname[TZNAME_MAX + 1] = "";
+
int neg;
- if(*s == '+')
+ if(s[0] == '+')
neg = 0;
- else if(*s == '-')
+ else if(s[0] == '-')
neg = 1;
else
{
@@ -120,40 +129,39 @@ iso_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
return NULL;
}
- char *o = s + 1;
+ size_t offname_i = 0;
+ offname[offname_i++] = *s++;
- if(isdigit(o[0]) && isdigit(o[1]))
+ if(isdigit(s[0]) && isdigit(s[1]))
{
- time->tm_gmtoff = (o[0] - '0') * 36000 + (o[1] - '0') * 3600;
- o += 2;
+ time->tm_gmtoff = (s[0] - '0') * 36000 + (s[1] - '0') * 3600;
+ offname[offname_i++] = *s++;
+ offname[offname_i++] = *s++;
}
else
{
- *errstr = "Invalid timezone offset, no digits after [+|-]";
+ *errstr = "Invalid timezone offset, no digits after <+|->";
return NULL;
}
- if(o[0] == ':') o++;
+ if(s[0] == ':') s++;
- if(isdigit(o[0]) && isdigit(o[1]))
+ if(isdigit(s[0]) && isdigit(s[1]))
{
- time->tm_gmtoff += (o[0] - '0') * 600 + (o[1] - '0') * 60;
- o += 2;
+ time->tm_gmtoff += (s[0] - '0') * 600 + (s[1] - '0') * 60;
+ offname[offname_i++] = *s++;
+ offname[offname_i++] = *s++;
}
else
{
- *errstr = "Invalid timezone offset, no digits after [+|-]";
+ *errstr = "Invalid timezone offset, no digits after <+|->HH[:]";
return NULL;
}
if(neg) time->tm_gmtoff = -time->tm_gmtoff;
-#ifndef TZNAME_MAX
-#define TZNAME_MAX _POSIX_TZNAME_MAX
-#endif
- static char offname[TZNAME_MAX + 1] = "";
- assert(o - s < TZNAME_MAX);
- memcpy(offname, s, o - s);
+ offname[offname_i++] = '\0';
+
time->tm_zone = offname;
}
}
diff --git a/test-cmd/date.sh b/test-cmd/date.sh
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: MPL-2.0
target="$(dirname "$0")/../cmd/date"
-plans=21
+plans=27
. "$(dirname "$0")/tap.sh"
. "$(dirname "$0")/init_env.sh"
@@ -58,6 +58,23 @@ t 'r_69' '-u -r 69 +%FT%T' '1970-01-01T00:01:09
t 'r_-69' '-u -r -69 +%FT%T' '1969-12-31T23:58:51
'
+t 'iso Date' '-u -d 2025-02-10T21:05:53,437742835+01:00 -Idate' '2025-02-10
+'
+
+t 'iso Hours' '-u -d 2025-02-10T21:05:53,437742835+01:00 -Ihours' '2025-02-10T20+00:00
+'
+
+t 'iso Minutes' '-u -d 2025-02-10T21:05:53,437742835+01:00 -Iminutes' '2025-02-10T20:05+00:00
+'
+
+t 'iso Seconds' '-u -d 2025-02-10T21:05:53,437742835+01:00 -Iseconds' '2025-02-10T20:05:53+00:00
+'
+
+t 'iso Nano-Seconds' '-u -d 2025-02-10T21:05:53,437742835+01:00 -Ins' '2025-02-10T20:05:53,437742835+00:00
+'
+
+t '+foo%%bar' '+foo%%bar' 'foo%bar
+'
#usage="\
#date [-uR] [-d datetime] [+format]