From 2c77ec54190294415c52a8da0c0d9c5a957c03a3 Mon Sep 17 00:00:00 2001 From: Conrad Meyer Date: Sat, 4 Aug 2018 21:54:30 +0000 Subject: [PATCH] date(1): Add ISO 8601 formatting option The new flag is named '-I'. It is documented in the manual page and covered by basic unit tests. --- bin/date/date.1 | 69 +++++++++++++++++- bin/date/date.c | 104 ++++++++++++++++++++++++--- bin/date/tests/format_string_test.sh | 57 +++++++++++++++ 3 files changed, 219 insertions(+), 11 deletions(-) diff --git a/bin/date/date.1 b/bin/date/date.1 index fa8bc211c3ac..355e4bf2d02b 100644 --- a/bin/date/date.1 +++ b/bin/date/date.1 @@ -32,7 +32,7 @@ .\" @(#)date.1 8.3 (Berkeley) 4/28/95 .\" $FreeBSD$ .\" -.Dd June 1, 2018 +.Dd August 4, 2018 .Dt DATE 1 .Os .Sh NAME @@ -64,6 +64,13 @@ .Nm .Op Fl d Ar dst .Op Fl t Ar minutes_west +.Nm +.Op Fl jnu +.Op Fl I Ns Op Ar FMT +.Op Fl f Ar input_fmt +.Op Fl r Ar ... +.Op Fl v Ar ... +.Op Ar new_date .Sh DESCRIPTION When invoked without arguments, the .Nm @@ -113,6 +120,33 @@ provided rather than using the default format. Parsing is done using .Xr strptime 3 . +.It Fl I Ns Op Ar FMT +Use +.St -iso8601 +output format. +.Ar FMT +may be omitted, in which case the default is +.Sq date . +Valid +.Ar FMT +values are +.Sq date , +.Sq hours , +.Sq minutes , +and +.Sq seconds . +The date and time is formatted to the specified precision. +When +.Ar FMT +is +.Sq hours +(or the more precise +.Sq minutes +or +.Sq seconds ) , +the +.St -iso8601 +format includes the timezone. .It Fl j Do not try to set the date. This allows you to use the @@ -401,6 +435,14 @@ sets the time to .Li "2:32 PM" , without modifying the date. .Pp +The command +.Pp +.Dl "TZ=America/Los_Angeles date -Iseconds -r 1533415339" +.Pp +will display +.Pp +.Dl "2018-08-04T13:42:19-07:00" +.Pp Finally the command: .Pp .Dl "date -j -f ""%a %b %d %T %Z %Y"" ""`date`"" ""+%s""" @@ -425,6 +467,19 @@ between and .Xr timed 8 fails. +.Pp +It is invalid to combine the +.Fl I +flag with either +.Fl R +or an output format +.Dq ( + Ns ... ) +operand. +If this occurs, +.Nm +prints: +.Ql multiple output formats specified +and exits with an error status. .Sh SEE ALSO .Xr locale 1 , .Xr gettimeofday 2 , @@ -443,12 +498,22 @@ The utility is expected to be compatible with .St -p1003.2 . The -.Fl d , f , j , n , r , t , +.Fl d , f , I , j , n , r , t , and .Fl v options are all extensions to the standard. +.Pp +The format selected by the +.Fl I +flag is compatible with +.St -iso8601 . .Sh HISTORY A .Nm command appeared in .At v1 . +.Pp +The +.Fl I +flag was added in +.Fx 12.0 . diff --git a/bin/date/date.c b/bin/date/date.c index 6d0e3d986f1e..a658ae1c38a6 100644 --- a/bin/date/date.c +++ b/bin/date/date.c @@ -51,6 +51,7 @@ __FBSDID("$FreeBSD$"); #include #include #include +#include #include #include #include @@ -68,10 +69,25 @@ __FBSDID("$FreeBSD$"); static time_t tval; int retval; -static void setthetime(const char *, const char *, int, int); static void badformat(void); +static void iso8601_usage(const char *); +static void multipleformats(void); +static void printdate(const char *); +static void printisodate(struct tm *); +static void setthetime(const char *, const char *, int, int); static void usage(void); +static const struct iso8601_fmt { + const char *refname; + const char *format_string; +} iso8601_fmts[] = { + { "date", "%Y-%m-%d" }, + { "hours", "T%H" }, + { "minutes", ":%M" }, + { "seconds", ":%S" }, +}; +static const struct iso8601_fmt *iso8601_selected; + static const char *rfc2822_format = "%a, %d %b %Y %T %z"; int @@ -79,7 +95,7 @@ main(int argc, char *argv[]) { struct timezone tz; int ch, rflag; - int jflag, nflag, Rflag; + bool Iflag, jflag, nflag, Rflag; const char *format; char buf[1024]; char *endptr, *fmt; @@ -89,15 +105,16 @@ main(int argc, char *argv[]) const struct vary *badv; struct tm *lt; struct stat sb; + size_t i; v = NULL; fmt = NULL; (void) setlocale(LC_TIME, ""); tz.tz_dsttime = tz.tz_minuteswest = 0; rflag = 0; - jflag = nflag = Rflag = 0; + Iflag = jflag = nflag = Rflag = 0; set_timezone = 0; - while ((ch = getopt(argc, argv, "d:f:jnRr:t:uv:")) != -1) + while ((ch = getopt(argc, argv, "d:f:I::jnRr:t:uv:")) != -1) switch((char)ch) { case 'd': /* daylight savings time */ tz.tz_dsttime = strtol(optarg, &endptr, 10) ? 1 : 0; @@ -108,6 +125,22 @@ main(int argc, char *argv[]) case 'f': fmt = optarg; break; + case 'I': + if (Rflag) + multipleformats(); + Iflag = 1; + if (optarg == NULL) { + iso8601_selected = iso8601_fmts; + break; + } + for (i = 0; i < nitems(iso8601_fmts); i++) + if (strcmp(optarg, iso8601_fmts[i].refname) == 0) + break; + if (i == nitems(iso8601_fmts)) + iso8601_usage(optarg); + + iso8601_selected = &iso8601_fmts[i]; + break; case 'j': jflag = 1; /* don't set time */ break; @@ -115,6 +148,8 @@ main(int argc, char *argv[]) nflag = 1; break; case 'R': /* RFC 2822 datetime format */ + if (Iflag) + multipleformats(); Rflag = 1; break; case 'r': /* user specified seconds */ @@ -163,6 +198,8 @@ main(int argc, char *argv[]) /* allow the operands in any order */ if (*argv && **argv == '+') { + if (Iflag) + multipleformats(); format = *argv + 1; ++argv; } @@ -173,8 +210,11 @@ main(int argc, char *argv[]) } else if (fmt != NULL) usage(); - if (*argv && **argv == '+') + if (*argv && **argv == '+') { + if (Iflag) + multipleformats(); format = *argv + 1; + } lt = localtime(&tval); if (lt == NULL) @@ -188,6 +228,9 @@ main(int argc, char *argv[]) } vary_destroy(v); + if (Iflag) + printisodate(lt); + if (format == rfc2822_format) /* * When using RFC 2822 datetime format, don't honor the @@ -196,12 +239,40 @@ main(int argc, char *argv[]) setlocale(LC_TIME, "C"); (void)strftime(buf, sizeof(buf), format, lt); + printdate(buf); +} + +static void +printdate(const char *buf) +{ (void)printf("%s\n", buf); if (fflush(stdout)) err(1, "stdout"); exit(retval); } +static void +printisodate(struct tm *lt) +{ + const struct iso8601_fmt *it; + char fmtbuf[32], buf[32], tzbuf[8]; + + fmtbuf[0] = 0; + for (it = iso8601_fmts; it <= iso8601_selected; it++) + strlcat(fmtbuf, it->format_string, sizeof(fmtbuf)); + + (void)strftime(buf, sizeof(buf), fmtbuf, lt); + + if (iso8601_selected > iso8601_fmts) { + (void)strftime(tzbuf, sizeof(tzbuf), "%z", lt); + memmove(&tzbuf[4], &tzbuf[3], 3); + tzbuf[3] = ':'; + strlcat(buf, tzbuf, sizeof(buf)); + } + + printdate(buf); +} + #define ATOI2(s) ((s) += 2, ((s)[-2] - '0') * 10 + ((s)[-1] - '0')) static void @@ -326,13 +397,28 @@ badformat(void) usage(); } +static void +iso8601_usage(const char *badarg) +{ + errx(1, "invalid argument '%s' for -I", badarg); +} + +static void +multipleformats(void) +{ + errx(1, "multiple output formats specified"); +} + static void usage(void) { - (void)fprintf(stderr, "%s\n%s\n", - "usage: date [-jnRu] [-d dst] [-r seconds] [-t west] " - "[-v[+|-]val[ymwdHMS]] ... ", + (void)fprintf(stderr, "%s\n%s\n%s\n", + "usage: date [-jnRu] [-d dst] [-r seconds|file] [-t west] " + "[-v[+|-]val[ymwdHMS]]", " " - "[-f fmt date | [[[[[cc]yy]mm]dd]HH]MM[.ss]] [+format]"); + "[-I[date | hours | minutes | seconds]]", + " " + "[-f fmt date | [[[[[cc]yy]mm]dd]HH]MM[.ss]] [+format]" + ); exit(1); } diff --git a/bin/date/tests/format_string_test.sh b/bin/date/tests/format_string_test.sh index 0bd4f1ea7b95..7ae323b50adc 100755 --- a/bin/date/tests/format_string_test.sh +++ b/bin/date/tests/format_string_test.sh @@ -48,6 +48,55 @@ ${desc}_test_body() { atf_add_test_case ${desc}_test } +iso8601_check() +{ + local arg flags exp_output_1 exp_output_2 + + arg="${1}" + flags="${2}" + exp_output_1="${3}" + exp_output_2="${4}" + + atf_check -o "inline:${exp_output_1}\n" \ + date $flags -r ${TEST1} "-I${arg}" + atf_check -o "inline:${exp_output_2}\n" \ + date $flags -r ${TEST2} "-I${arg}" +} + +iso8601_string_test() +{ + local desc arg exp_output_1 exp_output_2 flags + + desc="${1}" + arg="${2}" + flags="${3}" + exp_output_1="${4}" + exp_output_2="${5}" + + atf_test_case iso8601_${desc}_test + eval " +iso8601_${desc}_test_body() { + iso8601_check '${arg}' '${flags}' '${exp_output_1}' '${exp_output_2}' +}" + atf_add_test_case iso8601_${desc}_test + + if [ -z "$flags" ]; then + atf_test_case iso8601_${desc}_parity + eval " +iso8601_${desc}_parity_body() { + local exp1 exp2 + + atf_require_prog gdate + + exp1=\"\$(gdate --date '@${TEST1}' '-I${arg}')\" + exp2=\"\$(gdate --date '@${TEST2}' '-I${arg}')\" + + iso8601_check '${arg}' '' \"\${exp1}\" \"\${exp2}\" +}" + atf_add_test_case iso8601_${desc}_parity + fi +} + atf_init_test_cases() { format_string_test A A Saturday Monday @@ -89,4 +138,12 @@ atf_init_test_cases() format_string_test z z +0000 +0000 format_string_test percent % % % format_string_test plus + "Sat Feb 7 07:04:03 UTC 1970" "Mon Nov 12 21:20:00 UTC 2001" + + iso8601_string_test default "" "" "1970-02-07" "2001-11-12" + iso8601_string_test date date "" "1970-02-07" "2001-11-12" + iso8601_string_test hours hours "" "1970-02-07T07+00:00" "2001-11-12T21+00:00" + iso8601_string_test minutes minutes "" "1970-02-07T07:04+00:00" "2001-11-12T21:20+00:00" + iso8601_string_test seconds seconds "" "1970-02-07T07:04:03+00:00" "2001-11-12T21:20:00+00:00" + # BSD date(1) does not support fractional seconds at this time. + #iso8601_string_test ns ns "" "1970-02-07T07:04:03,000000000+00:00" "2001-11-12T21:20:00,000000000+00:00" }