>From 82c8b42de7bf9c69432ff175838f01f10008a512 Mon Sep 17 00:00:00 2001 From: Assaf Gordon Date: Thu, 25 Jul 2019 02:35:46 -0600 Subject: [PATCH 1/2] date: add --date-format=FORMAT option Parse -d=STRING dates using strptime(3) instead of gnulib's parse_datetime.c heuristics. Example: print the 100th day of 2019: $ date --date-format '%Y %j' --date '2019 100' +%F 2019-04-10 TODO: coreutils.texi, NEWS, usage * src/date.c (long_options): Add --date-format/STRP_FORMAT option. (parse_datetime_flags): Replace with ... (debug): ... new variable. (strp_format): New variable to hold the user-specified FORMAT string. (parse_datetime_string): New function, wrapper for parse_datetime2/strptime. (batch_convert, main): Call parse_datetime_string instead of parse_datetime2. (main): Handle STRP_FORMAT option. * tests/misc/date-strp.pl: New tests. * tests/local.mk (TESTS): Add date-strp.pl --- src/date.c | 78 ++++++++++++++++++++++--- tests/local.mk | 1 + tests/misc/date-strp.pl | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 tests/misc/date-strp.pl diff --git a/src/date.c b/src/date.c index d97d0ae52..4879474e3 100644 --- a/src/date.c +++ b/src/date.c @@ -80,7 +80,8 @@ static char const rfc_email_format[] = "%a, %d %b %Y %H:%M:%S %z"; enum { RFC_3339_OPTION = CHAR_MAX + 1, - DEBUG_DATE_PARSING + DEBUG_DATE_PARSING, + STRP_FORMAT }; static char const short_options[] = "d:f:I::r:Rs:u"; @@ -97,6 +98,7 @@ static struct option const long_options[] = {"rfc-2822", no_argument, NULL, 'R'}, {"rfc-3339", required_argument, NULL, RFC_3339_OPTION}, {"set", required_argument, NULL, 's'}, + {"date-format", required_argument, NULL, STRP_FORMAT}, {"uct", no_argument, NULL, 'u'}, {"utc", no_argument, NULL, 'u'}, {"universal", no_argument, NULL, 'u'}, @@ -105,8 +107,11 @@ static struct option const long_options[] = {NULL, 0, NULL, 0} }; -/* flags for parse_datetime2 */ -static unsigned int parse_datetime_flags; +static bool debug ; + +/* the strp format string specified by the user */ +static char* strp_format; + #if LOCALTIME_CACHE # define TZSET tzset () @@ -142,6 +147,9 @@ Display the current time in the given FORMAT, or set the system date.\n\ -d, --date=STRING display time described by STRING, not 'now'\n\ "), stdout); fputs (_("\ + --date-format=FORMAT parse -d,-f values according to FORMAT\n\ +"), stdout); + fputs (_("\ --debug annotate the parsed date,\n\ and warn about questionable usage to stderr\n\ "), stdout); @@ -281,6 +289,57 @@ Show the local time for 9AM next Friday on the west coast of the US\n\ exit (status); } +/* A wrapper calling either gnulib's parse_datetime2() or strptime(3), + depending on whether the user specified --date-format=FORMAT argument. */ +static bool +parse_datetime_string (struct timespec *result, char const *datestr, + timezone_t tzdefault, char const *tzstring) +{ + if (strp_format) + { + struct tm t; + time_t s = time (NULL); + localtime_rz (tzdefault, &s, &t); + char *endp = strptime (datestr, strp_format, &t ); + if (!endp) + { + if (debug) + error (0, 0, _("date string %s does not match format '%s'"), + quotearg (datestr), + strp_format); + return false; + } + + if (*endp) + { + if (debug) + error (EXIT_FAILURE, 0, _("extraneous characters in date " \ + "string: %s"), + quotearg (endp)); + return false; + } + + s = mktime (&t); + if (s == (time_t)-1) + { + error (0, errno, _("mktime failed")); + return false; + } + + *result = make_timespec (s, 0); + return true; + } + else + { + unsigned int parse_datetime_flags = debug ? PARSE_DATETIME_DEBUG : 0 ; + + return parse_datetime2 (result, datestr, NULL, + parse_datetime_flags, + tzdefault, tzstring); + } +} + + /* Parse each line in INPUT_FILENAME as with --date and display each resulting time and date. If the file cannot be opened, tell why then exit. Issue a diagnostic for any lines that cannot be parsed. @@ -322,8 +381,7 @@ batch_convert (const char *input_filename, const char *format, break; } - if (! parse_datetime2 (&when, line, NULL, - parse_datetime_flags, tz, tzstring)) + if (! parse_datetime_string (&when, line, tz, tzstring)) { if (line[line_length - 1] == '\n') line[line_length - 1] = '\0'; @@ -378,7 +436,7 @@ main (int argc, char **argv) datestr = optarg; break; case DEBUG_DATE_PARSING: - parse_datetime_flags |= PARSE_DATETIME_DEBUG; + debug = true; break; case 'f': batch_file = optarg; @@ -424,6 +482,9 @@ main (int argc, char **argv) set_datestr = optarg; set_date = true; break; + case STRP_FORMAT: + strp_format = optarg; + break; case 'u': /* POSIX says that 'date -u' is equivalent to setting the TZ environment variable, so this option should do nothing other @@ -548,9 +609,8 @@ main (int argc, char **argv) { if (set_datestr) datestr = set_datestr; - valid_date = parse_datetime2 (&when, datestr, NULL, - parse_datetime_flags, - tz, tzstring); + + valid_date = parse_datetime_string (&when, datestr, tz, tzstring); } } diff --git a/tests/local.mk b/tests/local.mk index e88d99f24..2a4f277ff 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -254,6 +254,7 @@ all_tests = \ tests/tail-2/tail-n0f.sh \ tests/misc/ls-misc.pl \ tests/misc/date.pl \ + tests/misc/date-strp.pl \ tests/misc/date-next-dow.pl \ tests/misc/ptx-overrun.sh \ tests/misc/xstrtol.pl \ diff --git a/tests/misc/date-strp.pl b/tests/misc/date-strp.pl new file mode 100644 index 000000000..4fc247cee --- /dev/null +++ b/tests/misc/date-strp.pl @@ -0,0 +1,151 @@ +#!/usr/bin/perl +# Test date's --date-format=FORMAT feature + +# Copyright (C) 2019 Free Software Foundation, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +use strict; + +(my $ME = $0) =~ s|.*/||; + +# Turn off localization of executable's output. +@ENV{qw(LANGUAGE LANG LC_ALL)} = ('C') x 3; + +# Export TZ=UTC0 so that zone-dependent strings match. +$ENV{TZ} = 'UTC0'; + +# NOTE: for all tests, we print only the parts that are parsed, +# as the other parts get the values from 'now' and will change +# every time. +my @Tests = + ( + ## + ## Date related formats specifiers (%Y, %b, %d, %M) + ## + ['d1', "--date-format '%Y' --date '2003' +%Y", {OUT=>"2003"}], + ['d2', "--date-format '%d %b %Y' --date '17 Feb 1979' +%F", + {OUT=>"1979-02-17"}], + ['d3', "--date-format '%b %Y,,, %d' --date 'Mar 1981,,, 13' +%F", + {OUT=>"1981-03-13"}], + ['d4', "--date-format '%y %d %m' --date '87 7 2' +%F", + {OUT=>"1987-02-07"}], + ['d5', "--date-format '%y %m %d' --date '87 02 07' +%F", + {OUT=>"1987-02-07"}], + + # Common case that frustrates users with the default parsing heuristics, + # see: https://www.gnu.org/software/coreutils/manual/html_node/Pure-numbers-in-date-strings.html + ['d6', "--date-format '%y%m%d' --date '010203' +%F", {OUT=>"2001-02-03"}], + ['d7', "--date-format '%y%d%m' --date '010203' +%F", {OUT=>"2001-03-02"}], + ['d8', "--date-format '%d%m%y' --date '010203' +%F", {OUT=>"2003-02-01"}], + ['d9', "--date-format '%d%y%m' --date '010203' +%F", {OUT=>"2002-03-01"}], + ['d10', "--date-format '%m%d%y' --date '010203' +%F", {OUT=>"2003-01-02"}], + ['d11', "--date-format '%m%y%d' --date '010203' +%F", {OUT=>"2002-01-03"}], + ['d12', "--date-format '%Y%m%d' --date '01020304' +%F", + {OUT=>"0102-03-04"}], + ['d13', "--date-format '%m%Y%d' --date '01020304' +%F", + {OUT=>"0203-01-04"}], + + # Tuesday (%A) of the 10th week (%W) in 2018 (%Y) + ['d20', "--date-format '%Y %W %A' --date '2018 10 Tue' +%F", + {OUT=>"2018-03-06"}], + # Same, with day-of-week (1..7), Monday=1 (%u) + ['d21', "--date-format '%Y %W %u' --date '2018 10 2' +%F", + {OUT=>"2018-03-06"}], + # Same, with day-of-week (0..6), Sunday=1 (%U) + ['d22', "--date-format '%Y %W %w' --date '2018 10 2' +%F", + {OUT=>"2018-03-06"}], + + # The 100th day of 2019 + ['d23', "--date-format '%Y %j' --date '2019 100' +%F", + {OUT=>"2019-04-10"}], + # Same, with funky separator + ['d24', "--date-format '%Y->%j' --date '2019->100' +%F", + {OUT=>"2019-04-10"}], + # Same, without separator + ['d25', "--date-format '%Y%j' --date '2019100' +%F", + {OUT=>"2019-04-10"}], + + + ## + ## Time related formats specifiers (%Y, %b, %d, %M) + ## + ['t1', "--date-format '%H^%M^%S' --date '13^14^15' +%T", + {OUT=>"13:14:15"}], + + # AM/PM + ['t2', "--date-format '%I:%M:%S%p' --date '03:09:00pm' +%T", + {OUT=>"15:09:00"}], + + # %T vs %H:%M:%S + ['t3', "--date-format '%T' --date '03:09:00' +%H:%M:%S", + {OUT=>"03:09:00"}], + ['t4', "--date-format '%H:%M:%S' --date '03:09:00' +%T", + {OUT=>"03:09:00"}], + + + ); + +# Append "\n" to each OUT=> RHS if the expected exit value is either +# zero or not specified (defaults to zero). +foreach my $t (@Tests) + { + my $exit_val; + foreach my $e (@$t) + { + ref $e && ref $e eq 'HASH' && defined $e->{EXIT} + and $exit_val = $e->{EXIT}; + } + foreach my $e (@$t) + { + ref $e && ref $e eq 'HASH' && defined $e->{OUT} && ! $exit_val + and $e->{OUT} .= "\n"; + } + } + +# Repeat all tests with --debug option, ensure it does not cause any regression +my @debug_tests; +foreach my $t (@Tests) + { + # Skip tests with EXIT!=0 or ERR_SUBST part + # (as '--debug' requires its own ERR_SUBST). + my $exit_val; + my $have_err_subst; + foreach my $e (@$t) + { + next unless ref $e && ref $e eq 'HASH'; + $exit_val = $e->{EXIT} if defined $e->{EXIT}; + $have_err_subst = 1 if defined $e->{ERR_SUBST}; + } + next if $exit_val || $have_err_subst; + + # Duplicate the test, add '--debug' argument + my @newt = @$t; + $newt[0] = 'dbg_' . $newt[0]; + $newt[1] = '--debug ' . $newt[1]; + + # Discard all debug printouts before comparing output + push @newt, {ERR_SUBST => q!s/^date: .*\n//m!}; + + push @debug_tests, \@newt; + } +push @Tests, @debug_tests; + + +my $save_temps = $ENV{DEBUG}; +my $verbose = $ENV{VERBOSE}; + +my $prog = 'date'; +my $fail = run_tests ($ME, $prog, \@Tests, $save_temps, $verbose); +exit $fail; -- 2.11.0