diff options
Diffstat (limited to 'src/date.c')
-rw-r--r-- | src/date.c | 664 |
1 files changed, 664 insertions, 0 deletions
diff --git a/src/date.c b/src/date.c new file mode 100644 index 0000000..9a282e2 --- /dev/null +++ b/src/date.c @@ -0,0 +1,664 @@ +/* date - print or set the system date and time + Copyright (C) 1989-2022 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 <https://www.gnu.org/licenses/>. + + David MacKenzie <djm@gnu.ai.mit.edu> */ + +#include <config.h> +#include <stdio.h> +#include <getopt.h> +#include <sys/types.h> +#if HAVE_LANGINFO_CODESET +# include <langinfo.h> +#endif + +#include "system.h" +#include "argmatch.h" +#include "die.h" +#include "error.h" +#include "parse-datetime.h" +#include "posixtm.h" +#include "quote.h" +#include "stat-time.h" +#include "fprintftime.h" + +/* The official name of this program (e.g., no 'g' prefix). */ +#define PROGRAM_NAME "date" + +#define AUTHORS proper_name ("David MacKenzie") + +static bool show_date (char const *, struct timespec, timezone_t); + +enum Time_spec +{ + /* Display only the date. */ + TIME_SPEC_DATE, + /* Display date, hours, minutes, and seconds. */ + TIME_SPEC_SECONDS, + /* Similar, but display nanoseconds. */ + TIME_SPEC_NS, + + /* Put these last, since they aren't valid for --rfc-3339. */ + + /* Display date and hour. */ + TIME_SPEC_HOURS, + /* Display date, hours, and minutes. */ + TIME_SPEC_MINUTES +}; + +static char const *const time_spec_string[] = +{ + /* Put "hours" and "minutes" first, since they aren't valid for + --rfc-3339. */ + "hours", "minutes", + "date", "seconds", "ns", NULL +}; +static enum Time_spec const time_spec[] = +{ + TIME_SPEC_HOURS, TIME_SPEC_MINUTES, + TIME_SPEC_DATE, TIME_SPEC_SECONDS, TIME_SPEC_NS +}; +ARGMATCH_VERIFY (time_spec_string, time_spec); + +/* A format suitable for Internet RFCs 5322, 2822, and 822. */ +static char const rfc_email_format[] = "%a, %d %b %Y %H:%M:%S %z"; + +/* For long options that have no equivalent short option, use a + non-character as a pseudo short option, starting with CHAR_MAX + 1. */ +enum +{ + DEBUG_DATE_PARSING_OPTION = CHAR_MAX + 1, + RESOLUTION_OPTION, + RFC_3339_OPTION +}; + +static char const short_options[] = "d:f:I::r:Rs:u"; + +static struct option const long_options[] = +{ + {"date", required_argument, NULL, 'd'}, + {"debug", no_argument, NULL, DEBUG_DATE_PARSING_OPTION}, + {"file", required_argument, NULL, 'f'}, + {"iso-8601", optional_argument, NULL, 'I'}, + {"reference", required_argument, NULL, 'r'}, + {"resolution", no_argument, NULL, RESOLUTION_OPTION}, + {"rfc-email", no_argument, NULL, 'R'}, + {"rfc-822", no_argument, NULL, 'R'}, + {"rfc-2822", no_argument, NULL, 'R'}, + {"rfc-3339", required_argument, NULL, RFC_3339_OPTION}, + {"set", required_argument, NULL, 's'}, + {"uct", no_argument, NULL, 'u'}, + {"utc", no_argument, NULL, 'u'}, + {"universal", no_argument, NULL, 'u'}, + {GETOPT_HELP_OPTION_DECL}, + {GETOPT_VERSION_OPTION_DECL}, + {NULL, 0, NULL, 0} +}; + +/* flags for parse_datetime2 */ +static unsigned int parse_datetime_flags; + +#if LOCALTIME_CACHE +# define TZSET tzset () +#else +# define TZSET /* empty */ +#endif + +#ifdef _DATE_FMT +# define DATE_FMT_LANGINFO() nl_langinfo (_DATE_FMT) +#else +# define DATE_FMT_LANGINFO() "" +#endif + +void +usage (int status) +{ + if (status != EXIT_SUCCESS) + emit_try_help (); + else + { + printf (_("\ +Usage: %s [OPTION]... [+FORMAT]\n\ + or: %s [-u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]]\n\ +"), + program_name, program_name); + fputs (_("\ +Display date and time in the given FORMAT.\n\ +With -s, or with [MMDDhhmm[[CC]YY][.ss]], set the date and time.\n\ +"), stdout); + + emit_mandatory_arg_note (); + + fputs (_("\ + -d, --date=STRING display time described by STRING, not 'now'\n\ +"), stdout); + fputs (_("\ + --debug annotate the parsed date,\n\ + and warn about questionable usage to stderr\n\ +"), stdout); + fputs (_("\ + -f, --file=DATEFILE like --date; once for each line of DATEFILE\n\ +"), stdout); + fputs (_("\ + -I[FMT], --iso-8601[=FMT] output date/time in ISO 8601 format.\n\ + FMT='date' for date only (the default),\n\ + 'hours', 'minutes', 'seconds', or 'ns'\n\ + for date and time to the indicated precision.\n\ + Example: 2006-08-14T02:34:56-06:00\n\ +"), stdout); + fputs (_("\ + --resolution output the available resolution of timestamps\n\ + Example: 0.000000001\n\ +"), stdout); + fputs (_("\ + -R, --rfc-email output date and time in RFC 5322 format.\n\ + Example: Mon, 14 Aug 2006 02:34:56 -0600\n\ +"), stdout); + fputs (_("\ + --rfc-3339=FMT output date/time in RFC 3339 format.\n\ + FMT='date', 'seconds', or 'ns'\n\ + for date and time to the indicated precision.\n\ + Example: 2006-08-14 02:34:56-06:00\n\ +"), stdout); + fputs (_("\ + -r, --reference=FILE display the last modification time of FILE\n\ +"), stdout); + fputs (_("\ + -s, --set=STRING set time described by STRING\n\ + -u, --utc, --universal print or set Coordinated Universal Time (UTC)\n\ +"), stdout); + fputs (HELP_OPTION_DESCRIPTION, stdout); + fputs (VERSION_OPTION_DESCRIPTION, stdout); + fputs (_("\ +\n\ +FORMAT controls the output. Interpreted sequences are:\n\ +\n\ + %% a literal %\n\ + %a locale's abbreviated weekday name (e.g., Sun)\n\ +"), stdout); + fputs (_("\ + %A locale's full weekday name (e.g., Sunday)\n\ + %b locale's abbreviated month name (e.g., Jan)\n\ + %B locale's full month name (e.g., January)\n\ + %c locale's date and time (e.g., Thu Mar 3 23:05:25 2005)\n\ +"), stdout); + fputs (_("\ + %C century; like %Y, except omit last two digits (e.g., 20)\n\ + %d day of month (e.g., 01)\n\ + %D date; same as %m/%d/%y\n\ + %e day of month, space padded; same as %_d\n\ +"), stdout); + fputs (_("\ + %F full date; like %+4Y-%m-%d\n\ + %g last two digits of year of ISO week number (see %G)\n\ + %G year of ISO week number (see %V); normally useful only with %V\n\ +"), stdout); + fputs (_("\ + %h same as %b\n\ + %H hour (00..23)\n\ + %I hour (01..12)\n\ + %j day of year (001..366)\n\ +"), stdout); + fputs (_("\ + %k hour, space padded ( 0..23); same as %_H\n\ + %l hour, space padded ( 1..12); same as %_I\n\ + %m month (01..12)\n\ + %M minute (00..59)\n\ +"), stdout); + fputs (_("\ + %n a newline\n\ + %N nanoseconds (000000000..999999999)\n\ + %p locale's equivalent of either AM or PM; blank if not known\n\ + %P like %p, but lower case\n\ + %q quarter of year (1..4)\n\ + %r locale's 12-hour clock time (e.g., 11:11:04 PM)\n\ + %R 24-hour hour and minute; same as %H:%M\n\ + %s seconds since the Epoch (1970-01-01 00:00 UTC)\n\ +"), stdout); + fputs (_("\ + %S second (00..60)\n\ + %t a tab\n\ + %T time; same as %H:%M:%S\n\ + %u day of week (1..7); 1 is Monday\n\ +"), stdout); + fputs (_("\ + %U week number of year, with Sunday as first day of week (00..53)\n\ + %V ISO week number, with Monday as first day of week (01..53)\n\ + %w day of week (0..6); 0 is Sunday\n\ + %W week number of year, with Monday as first day of week (00..53)\n\ +"), stdout); + fputs (_("\ + %x locale's date representation (e.g., 12/31/99)\n\ + %X locale's time representation (e.g., 23:13:48)\n\ + %y last two digits of year (00..99)\n\ + %Y year\n\ +"), stdout); + fputs (_("\ + %z +hhmm numeric time zone (e.g., -0400)\n\ + %:z +hh:mm numeric time zone (e.g., -04:00)\n\ + %::z +hh:mm:ss numeric time zone (e.g., -04:00:00)\n\ + %:::z numeric time zone with : to necessary precision (e.g., -04, +05:30)\n\ + %Z alphabetic time zone abbreviation (e.g., EDT)\n\ +\n\ +By default, date pads numeric fields with zeroes.\n\ +"), stdout); + fputs (_("\ +The following optional flags may follow '%':\n\ +\n\ + - (hyphen) do not pad the field\n\ + _ (underscore) pad with spaces\n\ + 0 (zero) pad with zeros\n\ + + pad with zeros, and put '+' before future years with >4 digits\n\ + ^ use upper case if possible\n\ + # use opposite case if possible\n\ +"), stdout); + fputs (_("\ +\n\ +After any flags comes an optional field width, as a decimal number;\n\ +then an optional modifier, which is either\n\ +E to use the locale's alternate representations if available, or\n\ +O to use the locale's alternate numeric symbols if available.\n\ +"), stdout); + fputs (_("\ +\n\ +Examples:\n\ +Convert seconds since the Epoch (1970-01-01 UTC) to a date\n\ + $ date --date='@2147483647'\n\ +\n\ +Show the time on the west coast of the US (use tzselect(1) to find TZ)\n\ + $ TZ='America/Los_Angeles' date\n\ +\n\ +Show the local time for 9AM next Friday on the west coast of the US\n\ + $ date --date='TZ=\"America/Los_Angeles\" 09:00 next Fri'\n\ +"), stdout); + emit_ancillary_info (PROGRAM_NAME); + } + exit (status); +} + +/* Yield the number of decimal digits needed to output a time with the + nanosecond resolution RES, without losing information. */ + +static int +res_width (long int res) +{ + int i = 9; + for (long long int r = 1; (r *= 10) <= res; ) + i--; + return i; +} + +/* Return a newly allocated copy of FORMAT with each "%-N" adjusted to + be "%9N", "%6N", or whatever other resolution is appropriate for + the current platform. If no "%-N" appears, return NULL. */ + +static char * +adjust_resolution (char const *format) +{ + char *copy = NULL; + + for (char const *f = format; *f; f++) + if (f[0] == '%') + { + if (f[1] == '-' && f[2] == 'N') + { + if (!copy) + copy = xstrdup (format); + copy[f + 1 - format] = '0' + res_width (gettime_res ()); + f += 2; + } + else + f += f[1] == '%'; + } + + return copy; +} + +/* 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. + Return true if successful. */ + +static bool +batch_convert (char const *input_filename, char const *format, + timezone_t tz, char const *tzstring) +{ + bool ok; + FILE *in_stream; + char *line; + size_t buflen; + struct timespec when; + + if (STREQ (input_filename, "-")) + { + input_filename = _("standard input"); + in_stream = stdin; + } + else + { + in_stream = fopen (input_filename, "r"); + if (in_stream == NULL) + { + die (EXIT_FAILURE, errno, "%s", quotef (input_filename)); + } + } + + line = NULL; + buflen = 0; + ok = true; + while (true) + { + ssize_t line_length = getline (&line, &buflen, in_stream); + if (line_length < 0) + { + /* FIXME: detect/handle error here. */ + break; + } + + if (! parse_datetime2 (&when, line, NULL, + parse_datetime_flags, tz, tzstring)) + { + if (line[line_length - 1] == '\n') + line[line_length - 1] = '\0'; + error (0, 0, _("invalid date %s"), quote (line)); + ok = false; + } + else + { + ok &= show_date (format, when, tz); + } + } + + if (fclose (in_stream) == EOF) + die (EXIT_FAILURE, errno, "%s", quotef (input_filename)); + + free (line); + + return ok; +} + +int +main (int argc, char **argv) +{ + int optc; + char const *datestr = NULL; + char const *set_datestr = NULL; + struct timespec when; + bool set_date = false; + char const *format = NULL; + bool get_resolution = false; + char *batch_file = NULL; + char *reference = NULL; + struct stat refstats; + bool ok; + + initialize_main (&argc, &argv); + set_program_name (argv[0]); + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEDIR); + textdomain (PACKAGE); + + atexit (close_stdout); + + while ((optc = getopt_long (argc, argv, short_options, long_options, NULL)) + != -1) + { + char const *new_format = NULL; + + switch (optc) + { + case 'd': + datestr = optarg; + break; + case DEBUG_DATE_PARSING_OPTION: + parse_datetime_flags |= PARSE_DATETIME_DEBUG; + break; + case 'f': + batch_file = optarg; + break; + case RESOLUTION_OPTION: + get_resolution = true; + break; + case RFC_3339_OPTION: + { + static char const rfc_3339_format[][32] = + { + "%Y-%m-%d", + "%Y-%m-%d %H:%M:%S%:z", + "%Y-%m-%d %H:%M:%S.%N%:z" + }; + enum Time_spec i = + XARGMATCH ("--rfc-3339", optarg, + time_spec_string + 2, time_spec + 2); + new_format = rfc_3339_format[i]; + break; + } + case 'I': + { + static char const iso_8601_format[][32] = + { + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S%:z", + "%Y-%m-%dT%H:%M:%S,%N%:z", + "%Y-%m-%dT%H%:z", + "%Y-%m-%dT%H:%M%:z" + }; + enum Time_spec i = + (optarg + ? XARGMATCH ("--iso-8601", optarg, time_spec_string, time_spec) + : TIME_SPEC_DATE); + new_format = iso_8601_format[i]; + break; + } + case 'r': + reference = optarg; + break; + case 'R': + new_format = rfc_email_format; + break; + case 's': + set_datestr = optarg; + set_date = true; + break; + case 'u': + /* POSIX says that 'date -u' is equivalent to setting the TZ + environment variable, so this option should do nothing other + than setting TZ. */ + if (putenv (bad_cast ("TZ=UTC0")) != 0) + xalloc_die (); + TZSET; + break; + case_GETOPT_HELP_CHAR; + case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS); + default: + usage (EXIT_FAILURE); + } + + if (new_format) + { + if (format) + die (EXIT_FAILURE, 0, _("multiple output formats specified")); + format = new_format; + } + } + + int option_specified_date = (!!datestr + !!batch_file + !!reference + + get_resolution); + + if (option_specified_date > 1) + { + error (0, 0, + _("the options to specify dates for printing are mutually exclusive")); + usage (EXIT_FAILURE); + } + + if (set_date && option_specified_date) + { + error (0, 0, + _("the options to print and set the time may not be used together")); + usage (EXIT_FAILURE); + } + + if (optind < argc) + { + if (optind + 1 < argc) + { + error (0, 0, _("extra operand %s"), quote (argv[optind + 1])); + usage (EXIT_FAILURE); + } + + if (argv[optind][0] == '+') + { + if (format) + die (EXIT_FAILURE, 0, _("multiple output formats specified")); + format = argv[optind++] + 1; + } + else if (set_date || option_specified_date) + { + error (0, 0, + _("the argument %s lacks a leading '+';\n" + "when using an option to specify date(s), any non-option\n" + "argument must be a format string beginning with '+'"), + quote (argv[optind])); + usage (EXIT_FAILURE); + } + } + + if (!format) + { + if (get_resolution) + format = "%s.%N"; + else + { + format = DATE_FMT_LANGINFO (); + + /* Do not wrap the following literal format string with _(...). + For example, suppose LC_ALL is unset, LC_TIME=POSIX, + and LANG="ko_KR". In that case, POSIX says that LC_TIME + determines the format and contents of date and time strings + written by date, which means "date" must generate output + using the POSIX locale; but adding _() would cause "date" + to use a Korean translation of the format. */ + if (! *format) + format = "%a %b %e %H:%M:%S %Z %Y"; + } + } + + char *format_copy = adjust_resolution (format); + char const *format_res = format_copy ? format_copy : format; + char const *tzstring = getenv ("TZ"); + timezone_t tz = tzalloc (tzstring); + + if (batch_file != NULL) + ok = batch_convert (batch_file, format_res, tz, tzstring); + else + { + bool valid_date = true; + ok = true; + + if (!option_specified_date && !set_date) + { + if (optind < argc) + { + /* Prepare to set system clock to the specified date/time + given in the POSIX-format. */ + set_date = true; + datestr = argv[optind]; + valid_date = posixtime (&when.tv_sec, + datestr, + (PDS_TRAILING_YEAR + | PDS_CENTURY | PDS_SECONDS)); + when.tv_nsec = 0; /* FIXME: posixtime should set this. */ + } + else + { + /* Prepare to print the current date/time. */ + gettime (&when); + } + } + else + { + /* (option_specified_date || set_date) */ + if (reference != NULL) + { + if (stat (reference, &refstats) != 0) + die (EXIT_FAILURE, errno, "%s", quotef (reference)); + when = get_stat_mtime (&refstats); + } + else if (get_resolution) + { + long int res = gettime_res (); + when.tv_sec = res / TIMESPEC_HZ; + when.tv_nsec = res % TIMESPEC_HZ; + } + else + { + if (set_datestr) + datestr = set_datestr; + valid_date = parse_datetime2 (&when, datestr, NULL, + parse_datetime_flags, + tz, tzstring); + } + } + + if (! valid_date) + die (EXIT_FAILURE, 0, _("invalid date %s"), quote (datestr)); + + if (set_date) + { + /* Set the system clock to the specified date, then regardless of + the success of that operation, format and print that date. */ + if (settime (&when) != 0) + { + error (0, errno, _("cannot set date")); + ok = false; + } + } + + ok &= show_date (format_res, when, tz); + } + + main_exit (ok ? EXIT_SUCCESS : EXIT_FAILURE); +} + +/* Display the date and/or time in WHEN according to the format specified + in FORMAT, followed by a newline. Return true if successful. */ + +static bool +show_date (char const *format, struct timespec when, timezone_t tz) +{ + struct tm tm; + + if (parse_datetime_flags & PARSE_DATETIME_DEBUG) + error (0, 0, _("output format: %s"), quote (format)); + + if (localtime_rz (tz, &when.tv_sec, &tm)) + { + if (format == rfc_email_format) + setlocale (LC_TIME, "C"); + fprintftime (stdout, format, &tm, tz, when.tv_nsec); + if (format == rfc_email_format) + setlocale (LC_TIME, ""); + fputc ('\n', stdout); + return true; + } + else + { + char buf[INT_BUFSIZE_BOUND (intmax_t)]; + error (0, 0, _("time %s is out of range"), + quote (timetostr (when.tv_sec, buf))); + return false; + } +} |