logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/
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:

Mcmd/date.1.in26+++++++++++++++++++++++++-
Mcmd/date.c104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mlib/iso_parse.c46+++++++++++++++++++++++++++-------------------
Mtest-cmd/date.sh19++++++++++++++++++-
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]