From: Assaf Gordon Date: Sun, 7 Aug 2016 00:08:32 +0000 (-0400) Subject: parse-datetime: add optional debug printing X-Git-Tag: v1.0~6694 X-Git-Url: https://gitweb.git.savannah.gnu.org/gitweb/?a=commitdiff_plain;h=12ad79069;p=gnulib.git parse-datetime: add optional debug printing Print parsing information, warnings, and errors to stderr. * lib/parse-datetime.h (parse_datetimte_debug): New global variable. * lib/parse-datetime.y: (DEBUG_*): Macros calling debug functions if debugging is enabled. (PROGRESS*): Same as DEBUG, for progress reporting. (dbg_printf): Print message to stderr, with 'date' prefix. (struct parser_control): Add 'debug_*_seen' variables. (str_days): Converts day ordinal/number to string (e.g. 'last wed'). (debug_print_current_time, debug_print_relateive_time): Prints the current/relative date/time value of parser_control. (YACC parser syntax): Print parsed parts with DEBUG_* macros. (to_year): Warn about 2-digit year parsing. (yylex): Warn about unrecognized words. (get_effective_timezone): Returns current timezone in minutes. (debug_strf{time,date,datetime}): Convert 'struct tm' to string as clearly and unambigiously as possible. (debug_mktime_not_ok): Print detailed information about failed date/time values. (parse_datetime): Add DEBUG messages for failures, warnings. Add PROGRESS messages for status messages. * modules/parse-datetime: Add 'timegm', 'gettext-h' dependencies. --- diff --git a/ChangeLog b/ChangeLog index 3bf441098a..07b00477dc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,28 @@ +2016-08-09 Assaf Gordon + + parse-datetime: add optional debug printing + Print parsing information, warnings, and errors to stderr. + * lib/parse-datetime.h (parse_datetimte_debug): New global variable. + * lib/parse-datetime.y: + (DEBUG_*): Macros calling debug functions if debugging is enabled. + (PROGRESS*): Same as DEBUG, for progress reporting. + (dbg_printf): Print message to stderr, with 'date' prefix. + (struct parser_control): Add 'debug_*_seen' variables. + (str_days): Converts day ordinal/number to string (e.g. 'last wed'). + (debug_print_current_time, debug_print_relateive_time): Prints the + current/relative date/time value of parser_control. + (YACC parser syntax): Print parsed parts with DEBUG_* macros. + (to_year): Warn about 2-digit year parsing. + (yylex): Warn about unrecognized words. + (get_effective_timezone): Returns current timezone in minutes. + (debug_strf{time,date,datetime}): Convert 'struct tm' to string as + clearly and unambigiously as possible. + (debug_mktime_not_ok): Print detailed information about failed + date/time values. + (parse_datetime): Add DEBUG messages for failures, warnings. Add + PROGRESS messages for status messages. + * modules/parse-datetime: Add 'timegm', 'gettext-h' dependencies. + 2016-08-06 Jim Meyering tests/init.sh: exclude dash with bad "local" semantics diff --git a/lib/parse-datetime.h b/lib/parse-datetime.h index c38b000cab..b849db3944 100644 --- a/lib/parse-datetime.h +++ b/lib/parse-datetime.h @@ -19,4 +19,6 @@ #include #include +extern bool parse_datetime_debug; + bool parse_datetime (struct timespec *, char const *, struct timespec const *); diff --git a/lib/parse-datetime.y b/lib/parse-datetime.y index 163a0eae71..34311cdf46 100644 --- a/lib/parse-datetime.y +++ b/lib/parse-datetime.y @@ -59,14 +59,18 @@ # undef static #endif +#include #include #include #include #include #include +#include "gettext.h" #include "xalloc.h" +#define _(str) gettext (str) + /* Bison's skeleton tests _STDLIB_H, while some stdlib.h headers use _STDLIB_H_ as witness. Map the latter to the one bison uses. */ /* FIXME: this is temporary. Remove when we have a mechanism to ensure @@ -105,6 +109,8 @@ #define HOUR(x) ((x) * 60) +#define STREQ(a, b) (strcmp (a, b) == 0) + /* long_time_t is a signed integer type that contains all time_t values. */ verify (TYPE_IS_INTEGER (time_t)); #if TIME_T_FITS_IN_LONG_INT @@ -118,6 +124,64 @@ typedef time_t long_time_t; errors that the cast doesn't. */ static unsigned char to_uchar (char ch) { return ch; } +/* Enable diagnostic debug output to STDERR */ +bool parse_datetime_debug = false; + +/* Debug message without parameters */ +#define DEBUG0(msg) \ + do { \ + if (parse_datetime_debug) \ + dbg_printf (msg); \ + } while (0) + +/* Debug message with printf-style parameters */ +#define DEBUG(x,...) \ + do { \ + if (parse_datetime_debug) \ + dbg_printf (x,__VA_ARGS__); \ + } while (0) + +/* Progress messages treated the same as debug messages */ +#define PROGRESS DEBUG +#define PROGRESS0 DEBUG0 + +/* Print the date/time of the parser_control struct */ +#define DEBUG_PRINT_CURRENT_TIME(item,pc) \ + do { \ + if (parse_datetime_debug) \ + debug_print_current_time (item, pc); \ + } while (0) + +/* Print the relative date/time of the parser control struct */ +#define DEBUG_PRINT_RELATIVE_TIME(item,pc) \ + do { \ + if (parse_datetime_debug) \ + debug_print_rel_time (item, pc); \ + } while (0) + +/* if 'mktime_ok' failed, print informative message with possible reasons */ +#define DEBUG_MKTIME_NOT_OK(tm0,tm1,pc,zones_seen) \ + do { \ + if (parse_datetime_debug) \ + debug_mktime_not_ok (tm0, tm1, pc, zones_seen); \ + } while (0) + + + +static void +dbg_printf (const char *msg,...) +{ + va_list args; + /* TODO: use gnulib's 'program_name' instead? */ + fputs ("date: ", stderr); + + va_start (args, msg); + vfprintf (stderr, msg, args); + va_end (args); +} + + + /* Lots of this code assumes time_t and time_t-like values fit into long_time_t. */ verify (TYPE_MINIMUM (long_time_t) <= TYPE_MINIMUM (time_t) @@ -209,6 +273,20 @@ typedef struct size_t times_seen; size_t zones_seen; + /* which of the 'seen' parts has been printed when debugging */ + size_t debug_dates_seen; + size_t debug_days_seen; + size_t debug_local_zones_seen; + size_t debug_dsts_seen; + size_t debug_times_seen; + size_t debug_zones_seen; + + /* true if the user specified explicit ordinal day value, */ + bool debug_ordinal_day_seen; + + /* the default input timezone, set by TZ value */ + long int debug_default_input_timezone; + /* Table of local time zone abbreviations, terminated by a null entry. */ table local_time_zone_table[3]; } parser_control; @@ -282,6 +360,201 @@ set_hhmmss (parser_control *pc, long int hour, long int minutes, pc->seconds.tv_nsec = nsec; } +/* returns a textual representation of the day ordinal/number values + in the parser_control struct (e.g. 'last wed', 'this tues', 'thu') */ +static const char* +str_days (parser_control *pc, char* /*output*/ buffer, size_t n) +{ + /* TODO: use the relative_time_table[] for reverse lookup */ + static const char* ordinal_values[] = { + "last", + "this", + "next/first", + "(SECOND)", /* SECOND is commented out in relative_time_table[] */ + "third", + "fourth", + "fifth", + "sixth", + "seventh", + "eight", + "ninth", + "tenth", + "eleventh", + "twelfth"}; + + static const char* days_values[] = { + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + }; + + /* don't add an ordinal prefix if the user didn't specify it + (e.g., "this wed" vs "wed") */ + if (pc->debug_ordinal_day_seen) + { + /* use word description of possible (e.g. -1 = last, 3 = third) */ + if (pc->day_ordinal>=-1 && pc->day_ordinal <=12) + { + strncpy (buffer, ordinal_values[ pc->day_ordinal+1 ], n); + buffer[n-1]='\0'; + } + else + { + snprintf (buffer,n,"%ld",pc->day_ordinal); + } + } + else + { + buffer[0] = '\0'; + } + + /* Add the day name */ + if (pc->day_number>=0 && pc->day_number<=6) + { + size_t l = strlen (buffer); + if (l>0) + { + strncat (buffer," ",n-l); + ++l; + } + strncat (buffer,days_values[pc->day_number],n-l); + } + else + { + /* invalid day_number value - should never happen */ + } + return buffer; +} + +/* debugging: print the current time in the parser_control structure. + The parser will increment "*_seen" members for those which were parsed. + This function will print only newly seen parts. */ +static void +debug_print_current_time (const char* item, parser_control *pc) +{ + char tmp[100] = {0}; + int space = 0; /* if true, add space delimiter */ + DEBUG (_("parsed %s part: "), item); /* no newline, more items printed below */ + + if (pc->dates_seen != pc->debug_dates_seen) + { + /*TODO: use pc->year.negative? */ + fprintf (stderr,"(Y-M-D) %04ld-%02ld-%02ld", + pc->year.value, pc->month, pc->day); + pc->debug_dates_seen = pc->dates_seen; + space = 1; + } + + if (pc->times_seen != pc->debug_times_seen) + { + if (space) + fputc (' ',stderr); + fprintf (stderr,"%02ld:%02ld:%02ld", + pc->hour, pc->minutes, pc->seconds.tv_sec); + if (pc->seconds.tv_nsec!=0) + fprintf (stderr,"%09ld", pc->seconds.tv_nsec); + if (pc->meridian==MERpm) + fputs ("pm",stderr); + + pc->debug_times_seen = pc->times_seen; + space = 1; + } + + if (pc->days_seen != pc->debug_days_seen) + { + if (space) + fputc (' ',stderr); + fprintf (stderr,_("%s (day ordinal=%ld number=%d)"), + str_days (pc,tmp,sizeof (tmp)), + pc->day_ordinal, pc->day_number); + pc->debug_days_seen = pc->days_seen ; + space = 1; + } + + if (pc->dsts_seen != pc->debug_dsts_seen) + { + if (space) + fputc (' ',stderr); + fprintf (stderr,_("is-dst=%d"), pc->local_isdst); + pc->dsts_seen = pc->debug_dsts_seen; + space = 1; + } + + /* TODO: fix incorrect display of EST=2:08h? */ + if (pc->zones_seen != pc->debug_zones_seen) + { + if (space) + fputc (' ',stderr); + fprintf (stderr,_("TZ=%+03d:%02d"), (int)(pc->time_zone/60), + abs ((int)pc->time_zone%60)); + pc->debug_zones_seen = pc->zones_seen; + space = 1; + } + + if (pc->local_zones_seen != pc->debug_local_zones_seen) + { + if (space) + fputc (' ',stderr); + fprintf (stderr,_("Local-TZ=%+03d:%02d"), (int)(pc->time_zone/60), + abs ((int)pc->time_zone%60)); + pc->debug_local_zones_seen = pc->local_zones_seen; + space = 1; + } + + if (pc->timespec_seen) + { + if (space) + fputc (' ',stderr); + fprintf (stderr,_("number of seconds: %ld"), pc->seconds.tv_sec); + } + + fputc ('\n', stderr); +} + +/* debugging: print the current relative values. */ +static void +debug_print_rel_time (const char* item, const parser_control *pc) +{ + int space = 0; /* if true, add space delimiter */ + DEBUG (_("parsed %s part: "), item); /* no newline, more items printed below */ + + if (pc->rel.year==0 && pc->rel.month==0 && pc->rel.day==0 + && pc->rel.hour==0 && pc->rel.minutes==00 && pc->rel.seconds == 0 + && pc->rel.ns==0) + { + /* Special case: relative time of this/today/now */ + fputs (_("today/this/now\n"),stderr); + return ; + } + +#define PRINT_REL_PART(x,name) \ + do { \ + if ( (pc->rel.x) != 0 ) \ + { \ + if (space) \ + fputc (' ',stderr); \ + fprintf (stderr,"%+ld %s", pc->rel.x, name); \ + space = 1; \ + } \ + } while (0) + + PRINT_REL_PART (year,"year(s)"); + PRINT_REL_PART (month,"month(s)"); + PRINT_REL_PART (day,"day(s)"); + PRINT_REL_PART (hour,"hour(s)"); + PRINT_REL_PART (minutes,"minutes"); + PRINT_REL_PART (seconds,"seconds"); + PRINT_REL_PART (ns,"nanoseconds"); + + fputc ('\n',stderr); +} + + + %} /* We want a reentrant parser, even if the TZ manipulation and the calls to @@ -330,6 +603,7 @@ timespec: { pc->seconds = $2; pc->timespec_seen = true; + DEBUG_PRINT_CURRENT_TIME (_("number of seconds"), pc); } ; @@ -340,20 +614,47 @@ items: item: datetime - { pc->times_seen++; pc->dates_seen++; } + { + pc->times_seen++; pc->dates_seen++; + DEBUG_PRINT_CURRENT_TIME (_("datetime"), pc); + } | time - { pc->times_seen++; } + { + pc->times_seen++; + DEBUG_PRINT_CURRENT_TIME (_("time"), pc); + } | local_zone - { pc->local_zones_seen++; } + { + pc->local_zones_seen++; + DEBUG_PRINT_CURRENT_TIME (_("local_zone"), pc); + } | zone - { pc->zones_seen++; } + { + pc->zones_seen++; + DEBUG_PRINT_CURRENT_TIME (_("zone"), pc); + } | date - { pc->dates_seen++; } + { + pc->dates_seen++; + DEBUG_PRINT_CURRENT_TIME (_("date"), pc); + } | day - { pc->days_seen++; } + { + pc->days_seen++; + DEBUG_PRINT_CURRENT_TIME (_("day"), pc); + } | rel + { + DEBUG_PRINT_RELATIVE_TIME (_("relative"), pc); + } | number + { + DEBUG_PRINT_RELATIVE_TIME (_("number"), pc); + } | hybrid + { + DEBUG_PRINT_RELATIVE_TIME (_("hybrid"), pc); + } ; datetime: @@ -463,11 +764,13 @@ day: { pc->day_ordinal = $1; pc->day_number = $2; + pc->debug_ordinal_day_seen = true; } | tUNUMBER tDAY { pc->day_ordinal = $1.value; pc->day_number = $2; + pc->debug_ordinal_day_seen = true; } ; @@ -486,12 +789,18 @@ date: you want portability, use the ISO 8601 format. */ if (4 <= $1.digits) { + DEBUG (_("warning: value %ld has %"PRIuMAX" digits. " \ + "Assuming YYYY/MM/DD\n"), $1.value, $1.digits); + pc->year = $1; pc->month = $3.value; pc->day = $5.value; } else { + DEBUG (_("warning: value %ld has less than 4 digits. " \ + "Assuming MM/DD/YY[YY]\n"), $1.value); + pc->month = $1.value; pc->day = $3.value; pc->year = $5; @@ -905,7 +1214,11 @@ to_year (textint textyear) /* XPG4 suggests that years 00-68 map to 2000-2068, and years 69-99 map to 1969-1999. */ else if (textyear.digits == 2) - year += year < 69 ? 2000 : 1900; + { + year += year < 69 ? 2000 : 1900; + DEBUG (_("warning: adjusting year value %ld to %ld\n"), + textyear.value, year); + } return year; } @@ -1170,7 +1483,10 @@ yylex (union YYSTYPE *lvalp, parser_control *pc) *p = '\0'; tp = lookup_word (pc, buff); if (! tp) - return '?'; + { + DEBUG (_("error: unknown word '%s'\n"), buff); + return '?'; + } lvalp->intval = tp->value; return tp->type; } @@ -1230,6 +1546,10 @@ mktime_ok (struct tm const *tm0, struct tm const *tm1, time_t t) Use heap allocation if TZ's length exceeds this. */ enum { TZBUFSIZE = 100 }; +/* A reasonable upper bound for the buffer used in debug print outs. + see days_to_name(), debug_strftime() and debug_mktime_not_ok() */ +enum { DBGBUFSIZE = 100 }; + /* Return a copy of TZ, stored in TZBUF if it fits, and heap-allocated otherwise. */ static char * @@ -1246,6 +1566,140 @@ get_tz (char tzbuf[TZBUFSIZE]) return tz; } +/* debugging: format a 'struct tm' into a buffer, taking the parser's + timezone information into account (if pc!=NULL). */ +static const char* +debug_strfdatetime (const struct tm *tm, const parser_control *pc, + char* /*output*/ buf, size_t n) +{ + /* TODO: + 1. find an optimal way to print date string in a clear and unambiguous + format. Currently, always add '(Y-M-D)' prefix. + Consider '2016y01m10d' or 'year(2016) month(01) day(10)'. + + If the user needs debug printing, it means he/she already having + issues with the parsing - better to avoid formats that could + be mis-interpreted (e.g. just YYYY-MM-DD). + + 2. Can strftime be used instead? + depends if it is portable and can print invalid dates on all systems. + + 3. Print timezone information ? + + 4. Print DST information ? + + 5. Print nanosecond information ? + + NOTE: + Printed date/time values might not be valid, e.g. '2016-02-31' + or '2016-19-2016' . These are the values as parsed from the user + string, before validation. + */ + int m = snprintf (buf,n,"(Y-M-D) %04d-%02d-%02d %02d:%02d:%02d", + tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + + /* if parser_control information was provided (for timezone), + and there's enough space in the buffer - add timezone info */ + if (pc != NULL && ((n-m)>0)) + { + const long int tz = (pc->zones_seen || pc->local_zones_seen) + ? pc->time_zone + : pc->debug_default_input_timezone; + snprintf (&buf[m],n-m," TZ=%+03d:%02d", (int)(tz/60), abs ((int)tz)%60); + } + return buf; +} + +static const char* +debug_strfdate (const struct tm *tm, char* /*output*/ buf, size_t n) +{ + snprintf (buf,n,"(Y-M-D) %04d-%02d-%02d", + tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday); + return buf; +} + +static const char* +debug_strftime (const struct tm *tm, char* /*output*/ buf, size_t n) +{ + snprintf (buf,n,"%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + return buf; +} + +/* If 'mktime_ok()' failed, display the failed time values, + and provide possible hints. Example output: + + date: error: invalid date/time value: + date: user provided time: '(Y-M-D) 2006-04-02 02:45:00' + date: normalized time: '(Y-M-D) 2006-04-02 03:45:00' + date: __ + date: possible reasons: + date: non-existing due to daylight-saving time; + date: numeric values overflow; + date: missing timezone; + */ +static void +debug_mktime_not_ok (struct tm const *tm0, struct tm const *tm1, + const parser_control *pc, bool time_zone_seen) +{ + /* TODO: handle t==-1 (as in 'mktime_ok') */ + char tmp[DBGBUFSIZE]; + int i; + const bool eq_sec = (tm0->tm_sec == tm1->tm_sec); + const bool eq_min = (tm0->tm_min == tm1->tm_min); + const bool eq_hour = (tm0->tm_hour == tm1->tm_hour); + const bool eq_mday = (tm0->tm_mday == tm1->tm_mday); + const bool eq_month = (tm0->tm_mon == tm1->tm_mon); + const bool eq_year = (tm0->tm_year == tm1->tm_year); + + const bool dst_shift = eq_sec && eq_min && !eq_hour + && eq_mday && eq_month && eq_year; + + DEBUG0 (_("error: invalid date/time value:\n")); + DEBUG (_(" user provided time: '%s'\n"), + debug_strfdatetime (tm0, pc, tmp, sizeof (tmp))); + DEBUG (_(" normalized time: '%s'\n"), + debug_strfdatetime (tm1, pc, tmp, sizeof (tmp))); + /* NOTEs: the format must be aligned with debug_strfdatetime() and the two + DEBUG statements above. this string is not translated. */ + i = snprintf (tmp, sizeof(tmp), + " %4s %2s %2s %2s %2s %2s", + eq_year?"":"----", eq_month?"":"--", eq_mday?"":"--", + eq_hour?"":"--", eq_min?"":"--", eq_sec?"":"--"); + /* Trim trailing whitespace */ + if ((i>0) && (i0) && (tmp[i-1]==' ')) + --i; + tmp[i] = '\0'; + } + DEBUG ("%s\n", tmp); + + DEBUG0 (_(" possible reasons:\n")); + if (dst_shift) + DEBUG0 (_(" non-existing due to daylight-saving time;\n")); + if (!eq_mday && !eq_month) + DEBUG0 (_(" invalid day/month combination;\n")); + DEBUG0 (_(" numeric values overflow;\n")); + DEBUG (" %s\n",time_zone_seen?_("incorrect timezone") + :_("missing timezone")); +} + + +/* Returns the effective local timezone, in minutes. */ +static long int +get_effective_timezone (void) +{ + /* TODO: check for failures */ + const time_t z = 0; + time_t lz ; + struct tm *ltm; + ltm = localtime (&z); + lz = timegm (ltm)/60; + return (long int)lz; +} + + /* Parse a date/time string, storing the resulting time value into *RESULT. The string itself is pointed to by P. Return true if successful. P can be an incomplete or relative time specification; if so, use @@ -1266,6 +1720,8 @@ parse_datetime (struct timespec *result, char const *p, char *tz0 = NULL; char tz0buf[TZBUFSIZE]; bool ok = true; + char dbg_ord[DBGBUFSIZE]; + char dbg_tm[DBGBUFSIZE]; if (! now) { @@ -1351,6 +1807,14 @@ parse_datetime (struct timespec *result, char const *p, pc.local_zones_seen = 0; pc.dsts_seen = 0; pc.zones_seen = 0; + pc.debug_dates_seen = 0; + pc.debug_days_seen = 0; + pc.debug_times_seen = 0; + pc.debug_local_zones_seen = 0; + pc.debug_dsts_seen = 0; + pc.debug_zones_seen = 0; + pc.debug_ordinal_day_seen = false; + pc.debug_default_input_timezone = 0; #if HAVE_STRUCT_TM_TM_ZONE pc.local_time_zone_table[0].name = tmp->tm_zone; @@ -1410,8 +1874,60 @@ parse_datetime (struct timespec *result, char const *p, pc.local_time_zone_table[1].name = NULL; } + pc.debug_default_input_timezone = get_effective_timezone (); + if (yyparse (&pc) != 0) - goto fail; + { + DEBUG (_("error: parsing failed, stopped at '%s'\n"), pc.input); + goto fail; + } + + /* determine effective timezone source */ + if (parse_datetime_debug) + { + long int tz = pc.debug_default_input_timezone; + const char* tz_env; + const char* tz_src; + + if (pc.timespec_seen) + { + tz = 0 ; + tz_src = _("'@timespec' - always UTC0"); + } + else if (pc.local_zones_seen || pc.zones_seen) + { + tz = pc.time_zone; + tz_src = _("parsed date/time string"); + } + else if ((tz_env = getenv("TZ"))) + { + if (tz_was_altered) + { + snprintf (dbg_tm, sizeof(dbg_tm), _("TZ=\"%s\" in date string"), + tz_env); + tz_src = dbg_tm; + } + else if (STREQ(tz_env,"UTC0")) + { + /* Special case: using 'date -u' simply set TZ=UTC0 */ + tz_src = _("TZ=UTC0 environment value or -u"); + } + else + { + snprintf (dbg_tm, sizeof(dbg_tm), + _("TZ=\"%s\" environment value"), tz_env); + tz_src = dbg_tm; + } + } + else + { + tz_src = _("system default"); + } + + PROGRESS (_("input timezone: %+03d:%02d (set from %s)\n"), + (int)(tz/60), abs ((int)tz)%60, tz_src); + + } if (pc.timespec_seen) *result = pc.seconds; @@ -1419,7 +1935,22 @@ parse_datetime (struct timespec *result, char const *p, { if (1 < (pc.times_seen | pc.dates_seen | pc.days_seen | pc.dsts_seen | (pc.local_zones_seen + pc.zones_seen))) - goto fail; + { + if (parse_datetime_debug) + { + if (pc.times_seen > 1) + DEBUG0 ("error: seen multiple time parts\n"); + if (pc.dates_seen > 1) + DEBUG0 ("error: seen multiple date parts\n"); + if (pc.days_seen > 1) + DEBUG0 ("error: seen multiple days parts\n"); + if (pc.dsts_seen > 1) + DEBUG0 ("error: seen multiple daylight-saving parts\n"); + if ( (pc.local_zones_seen + pc.zones_seen) > 1) + DEBUG0 ("error: seen multiple time-zone parts\n"); + } + goto fail; + } tm.tm_year = to_year (pc.year) - TM_YEAR_BASE; tm.tm_mon = pc.month - 1; @@ -1428,14 +1959,24 @@ parse_datetime (struct timespec *result, char const *p, { tm.tm_hour = to_hour (pc.hour, pc.meridian); if (tm.tm_hour < 0) - goto fail; + { + const char* mrd = (pc.meridian==MERam)?"am": + (pc.meridian==MERpm)?"pm":""; + DEBUG (_("error: invalid hour %ld%s\n"), pc.hour, mrd); + + goto fail; + } tm.tm_min = pc.minutes; tm.tm_sec = pc.seconds.tv_sec; + PROGRESS (_("using %s time as starting value: '%s'\n"), + (pc.times_seen)?_("specified"):_("current"), + debug_strftime (&tm,dbg_tm,sizeof (dbg_tm))); } else { tm.tm_hour = tm.tm_min = tm.tm_sec = 0; pc.seconds.tv_nsec = 0; + DEBUG0 ("warning: using midnight as starting time: 00:00:00\n"); } /* Let mktime deduce tm_isdst if we have an absolute time stamp. */ @@ -1454,7 +1995,11 @@ parse_datetime (struct timespec *result, char const *p, if (! mktime_ok (&tm0, &tm, Start)) { if (! pc.zones_seen) - goto fail; + { + DEBUG_MKTIME_NOT_OK (&tm0, &tm, &pc, pc.zones_seen); + + goto fail; + } else { /* Guard against falsely reporting errors near the time_t @@ -1478,12 +2023,21 @@ parse_datetime (struct timespec *result, char const *p, sprintf (tz1buf, "XXX%s%ld:%02d", &"-"[time_zone < 0], abs_time_zone_hour, abs_time_zone_min); if (setenv ("TZ", tz1buf, 1) != 0) - goto fail; + { + /* TODO: was warn () + print errno? */ + DEBUG (_("error: setenv('TZ','%s') failed\n"), tz1buf); + + goto fail; + } tz_was_altered = true; tm = tm0; Start = mktime (&tm); if (! mktime_ok (&tm0, &tm, Start)) - goto fail; + { + DEBUG_MKTIME_NOT_OK (&tm0, &tm, &pc, pc.zones_seen); + + goto fail; + } } } @@ -1496,19 +2050,58 @@ parse_datetime (struct timespec *result, char const *p, tm.tm_isdst = -1; Start = mktime (&tm); if (Start == (time_t) -1) - goto fail; + { + DEBUG (_("error: day '%s' (day ordinal=%ld number=%d) " \ + "resulted in an invalid date: '%s'\n"), + str_days (&pc,dbg_ord,sizeof (dbg_ord)), + pc.day_ordinal,pc.day_number, + debug_strfdatetime (&tm, &pc, dbg_tm,sizeof (dbg_tm))); + + goto fail; + } + + PROGRESS (_("new start date: '%s' is '%s'\n"), + str_days (&pc,dbg_ord,sizeof (dbg_ord)), + debug_strfdatetime (&tm, &pc, dbg_tm,sizeof (dbg_tm))); + } + if (!pc.dates_seen && !pc.days_seen) + PROGRESS (_("using current date as starting value: '%s'\n"), + debug_strfdate (&tm,dbg_tm,sizeof (dbg_tm))); + + if (pc.days_seen && pc.dates_seen) + DEBUG (_("warning: day (%s) ignored when explicit dates are given\n"), + str_days (&pc,dbg_ord,sizeof (dbg_ord))); + + PROGRESS (_("starting date/time: '%s'\n"), + debug_strfdatetime (&tm, &pc, dbg_tm,sizeof (dbg_tm))); + + /* Add relative date. */ if (pc.rel.year | pc.rel.month | pc.rel.day) { + if ((pc.rel.year != 0 || pc.rel.month !=0) && tm.tm_mday==1) + DEBUG0 ("warning: when adding relative months/years, " \ + "it is recommended to specify the 15th of the months\n"); + + + if (pc.rel.day != 0 && tm.tm_hour==0) + DEBUG0 ("warning: when adding relative days, " \ + "it is recommended to specify 12:00pm\n"); + int year = tm.tm_year + pc.rel.year; int month = tm.tm_mon + pc.rel.month; int day = tm.tm_mday + pc.rel.day; if (((year < tm.tm_year) ^ (pc.rel.year < 0)) | ((month < tm.tm_mon) ^ (pc.rel.month < 0)) | ((day < tm.tm_mday) ^ (pc.rel.day < 0))) - goto fail; + { + /* TODO: what is the actual error? int-value wrap-around? */ + DEBUG (_("error: %s:%d\n"), __FILE__,__LINE__); + + goto fail; + } tm.tm_year = year; tm.tm_mon = month; tm.tm_mday = day; @@ -1518,7 +2111,20 @@ parse_datetime (struct timespec *result, char const *p, tm.tm_isdst = tm0.tm_isdst; Start = mktime (&tm); if (Start == (time_t) -1) - goto fail; + { + DEBUG (_("error: adding relative date resulted " \ + "in an invalid date: '%s'\n"), + debug_strfdatetime (&tm, &pc, dbg_tm, sizeof (dbg_tm))); + + goto fail; + } + + PROGRESS (_("after date adjustment "\ + "(%+ld years, %+ld months, %+ld days),\n"), + pc.rel.year, pc.rel.month, pc.rel.day); + PROGRESS (_(" new date/time = '%s'\n"), + debug_strfdatetime (&tm, &pc, dbg_tm, sizeof (dbg_tm))); + } /* The only "output" of this if-block is an updated Start value, @@ -1533,15 +2139,29 @@ parse_datetime (struct timespec *result, char const *p, time_t t = Start; struct tm const *gmt = gmtime (&t); if (! gmt) - goto fail; + { + /* TODO: use 'warn(3)' + print errno ? */ + DEBUG (_("error: gmtime failed for t=%ld\n"),t); + + goto fail; + } delta -= tm_diff (&tm, gmt); #endif t1 = Start - delta; if ((Start < t1) != (delta < 0)) - goto fail; /* time_t overflow */ + { + DEBUG (_("error: timezone %ld caused time_t overflow\n"), + pc.time_zone); + + goto fail; /* time_t overflow */ + } Start = t1; } + PROGRESS (_("'%s' = %ld epoch-seconds\n"), + debug_strfdatetime (&tm, &pc, dbg_tm, sizeof (dbg_tm)), + Start); + /* Add relative hours, minutes, and seconds. On hosts that support leap seconds, ignore the possibility of leap seconds; e.g., "+ 10 minutes" adds 600 seconds, even if one of them is a @@ -1570,7 +2190,19 @@ parse_datetime (struct timespec *result, char const *p, | ((t3 < t2) ^ (d3 < 0)) | ((t4 < t3) ^ (d4 < 0)) | (t5 != t4)) - goto fail; + { + DEBUG0 (" error: adding relative time caused an overflow\n"); + + goto fail; + } + + if (pc.rel.hour | pc.rel.minutes | pc.rel.seconds | pc.rel.ns ) + { + PROGRESS (_("after time adjustment " \ + "(%+ld hours, %+ld minutes, %+ld seconds, %+ld ns),\n"), + pc.rel.hour,pc.rel.minutes,pc.rel.seconds,pc.rel.ns); + PROGRESS (_(" new time = %ld epoch-seconds\n"),t5); + } result->tv_sec = t5; result->tv_nsec = normalized_ns; @@ -1586,6 +2218,49 @@ parse_datetime (struct timespec *result, char const *p, ok &= (tz0 ? setenv ("TZ", tz0, 1) : unsetenv ("TZ")) == 0; if (tz0 != tz0buf) free (tz0); + + if (ok && parse_datetime_debug) + { + /* print local timezone AFTER restoring TZ (if tz_was_altered)*/ + const long int otz = get_effective_timezone (); + const char* tz_src; + const char* tz_env; + + if ((tz_env = getenv("TZ"))) + { + /* Special case: using 'date -u' simply set TZ=UTC0 */ + if (STREQ(tz_env,"UTC0")) + { + tz_src = _("TZ=UTC0 envionment value or -u"); + } + else + { + snprintf (dbg_tm, sizeof(dbg_tm), + _("TZ=\"%s\" environment value"), tz_env); + tz_src = dbg_tm; + } + } + else + { + tz_src = _("system default"); + } + + PROGRESS (_("output timezone: %+03d:%02d (set from %s)\n"), + (int)(otz/60), abs ((int)otz)%60, tz_src); + + + PROGRESS (_("final: %ld.%09ld (epoch-seconds)\n"), + result->tv_sec,result->tv_nsec); + + struct tm const *gmt = gmtime (&result->tv_sec); + PROGRESS (_("final: %s (UTC0)\n"), + debug_strfdatetime (gmt, NULL, dbg_tm, sizeof (dbg_tm))); + struct tm const *lmt = localtime (&result->tv_sec); + PROGRESS (_("final: %s (output timezone TZ=%+03d:%02d)\n"), + debug_strfdatetime (lmt, NULL, dbg_tm, sizeof (dbg_tm)), + (int)(otz/60), abs ((int)otz)%60); + } + return ok; } diff --git a/modules/parse-datetime b/modules/parse-datetime index fb481369e2..72b41dd7ad 100644 --- a/modules/parse-datetime +++ b/modules/parse-datetime @@ -13,11 +13,13 @@ Depends-on: c-ctype stdbool gettime +gettext-h intprops mktime setenv unsetenv time +timegm verify xalloc