Skip to content

Commit

Permalink
date(1): Add ISO 8601 formatting option
Browse files Browse the repository at this point in the history
The new flag is named '-I'.  It is documented in the manual page and covered
by basic unit tests.
  • Loading branch information
cemeyer committed Aug 4, 2018
1 parent 7997bd2 commit 627fbf8
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 11 deletions.
69 changes: 67 additions & 2 deletions bin/date/date.1
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"""
Expand All @@ -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 ,
Expand All @@ -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 .
104 changes: 95 additions & 9 deletions bin/date/date.c
Expand Up @@ -51,6 +51,7 @@ __FBSDID("$FreeBSD$");
#include <ctype.h>
#include <err.h>
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand All @@ -68,18 +69,33 @@ __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
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;
Expand All @@ -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;
Expand All @@ -108,13 +125,31 @@ 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;
case 'n': /* don't set network */
nflag = 1;
break;
case 'R': /* RFC 2822 datetime format */
if (Iflag)
multipleformats();
Rflag = 1;
break;
case 'r': /* user specified seconds */
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]]",
" "
"[-I[date | hours | minutes | seconds]]",
" "
"[-f fmt date | [[[[[cc]yy]mm]dd]HH]MM[.ss]] [+format]");
"[-f fmt date | [[[[[cc]yy]mm]dd]HH]MM[.ss]] [+format]"
);
exit(1);
}
57 changes: 57 additions & 0 deletions bin/date/tests/format_string_test.sh
Expand Up @@ -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
Expand Down Expand Up @@ -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"
}

0 comments on commit 627fbf8

Please sign in to comment.