'date' for Bash and Unix

The Unix date command is quite powerful. And with great power comes great complexity (were you expecting "responsibility?" - you're a comic geek). I'll cover several things in this blog entry:

Auxiliary Notes:

Basic Use

$ date
Thu Feb 16 20:58:15 EST 2017

It's surprisingly smart (the GNU Tools version on Linux ... not so much on Mac):

$ date --date='a week tuesday'
Mon Feb 27 20:00:00 EST 2017

Feed it any old date and get it back out in date's preferred format:

$ date -d 2018-01-31
Wed Jan 31 00:00:00 EST 2018

'-d' is the same as '--date'.

Date Formatting

I don't like the long and ugly format date defaults to - but that's okay, because it's easily improved. (Formatting options are the same on Mac and Linux).

$ date "+%Y-%m-%d"
2017-02-16
$ date "+%H%M"
2115

The latter is the current time in 24 hour format. Note that you can insert any characters you like (the per cent sign is tricky, but it can be done as well).

$ date "+%Z %z" # let's find out our time zone
EST -0500

See strftime Formatting below for the gory details of the formatting.

Reading Dates

That's all fairly simple (lots of options, but relatively straight-forward). Now let's get to the hairy stuff. What if I have a date from an outside source and need to be able to understand it not just as a string, but as a date? This whole adventure in dates started because of TLS certificates. To find out when one expires programmatically, we do this:

$ echo | openssl s_client -connect "www.google.ca:443" 2>/dev/null | \
> openssl x509 -noout -dates | grep notAfter | awk 'BEGIN { FS="=" } { print $2 }'
Apr 26 13:21:00 2017 GMT

I'm not going to explain that code - this is just about the dates. Assume we've got an accurate date for the expiry of www.google.ca's TLS certificate. How can we make Bash and/or date comprehend that ugly string? In Linux, this is magically easy:

$ date --date 'Apr 26 13:21:00 2017 GMT' "+%Y-%m-%d"
2017-04-26

The '--date' parameter will take all kinds of date formats and figure them out. This DOES NOT work on a Mac, where it's much trickier - their version of date doesn't include '--date'. We have to specify the exact format to be read in:

$ date -jf '%b %e %H:%M:%S %Y %Z' "Apr 26 13:21:00 2017 GMT" "+%Y-%m-%d"
2017-04-26

This DOES NOT work on Linux, the switches are different. Figuring out the exact format string to match that date format was ... entertaining.

Date Math

And now for something even hairier. This is the real meat of the problem: how long until that TLS certificate expires?

When I first heard of "Unix time," aka "the number of seconds since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970," I thought someone was shitting me. It sounded like a completely ridiculous system. Hell, it's going to completely break down in 2038 because the size of the integer used will max out that year. But the more I think about time systems and how totally insane they are ... In fact, let's pause to think about that:

  • 60 seconds to the minute
  • 60 minutes to the hour
  • 24 hours to the day
  • 7 days a week

Oooh, glad we picked round, easily divisible and sane numbers ...

  • 28, 29, 30, or 31 days to the month
  • 365 days a year except when there's a leap day
  • and let's not forget those pesky leap seconds

Many a fine programmer has gone insane when s/he decided to write a date library. Trust me on this: let someone else write that library.

The more I think about our time systems, the more "seconds since the Epoch" makes sense. It's a single point of reference for an otherwise unfathomably complex system.

So here's how we do date math (I'm talking months here, maybe a couple years: if you're looking for centuries, "seconds since the Epoch" won't work for you). Get date to give you both your dates as seconds since the Epoch:

$ date --date 'Apr 26 13:21:00 2017 GMT' "+%s"
1493212860
$ date "+%s" # Now:
1487299138

Finally, subtract the two and divide by the number of seconds in a day:

$ echo "$(( $((1493212860 - 1487299138)) / $((60*60*24)) ))"
68

Google's TLS certificate will expire in 68 days.


strftime Formatting

This is ripped right out of the middle of Linux's :ss:'man date': it's incomplete, but generally this is the most important stuff. If you're using a Mac and don't have the GNU Tools installed, you may have to :ss:'man strftime' to get this information. To my knowledge, these are identical on both Mac and Linux - in fact, I think these formatting options are used in multiple languages - anything that uses C strftime underneath. This includes at least C itself, Python, and Ruby.

%%a literal %
%alocale's abbreviated weekday name (e.g., Sun)
%Alocale's full weekday name (e.g., Sunday)
%blocale's abbreviated month name (e.g., Jan)
%Blocale's full month name (e.g., January)
%clocale's date and time (e.g., Thu Mar 3 23:05:25 2005)
%Ccentury; like %Y, except omit last two digits (e.g., 20)
%dday of month (e.g., 01)
%Ddate; same as %m/%d/%y
%eday of month, space padded; same as %_d
%Ffull date; same as %Y-%m-%d
%glast two digits of year of ISO week number (see %G)
%Gyear of ISO week number (see %V); normally useful only with %V
%hsame as %b
%Hhour (00..23)
%Ihour (01..12)
%jday of year (001..366)
%khour, space padded ( 0..23); same as %_H
%lhour, space padded ( 1..12); same as %_I
%mmonth (01..12)
%Mminute (00..59)
%na newline
%Nnanoseconds (000000000..999999999)
%plocale's equivalent of either AM or PM; blank if not known
%Plike %p, but lower case
%rlocale's 12-hour clock time (e.g., 11:11:04 PM)
%R24-hour hour and minute; same as %H:%M
%sseconds since 1970-01-01 00:00:00 UTC
%Ssecond (00..60)
%ta tab
%Ttime; same as %H:%M:%S
%uday of week (1..7); 1 is Monday
%Uweek number of year, with Sunday as first day of week (00..53)
%VISO week number, with Monday as first day of week (01..53)
%wday of week (0..6); 0 is Sunday
%Wweek number of year, with Monday as first day of week (00..53)
%xlocale's date representation (e.g., 12/31/99)
%Xlocale's time representation (e.g., 23:13:48)
%ylast two digits of year (00..99)
%Yyear
%z+hhmm numeric time zone (e.g., -0400)
%:z+hh:mm numeric time zone (e.g., -04:00)
%::z+hh:mm:ss numeric time zone (e.g., -04:00:00)
%:::znumeric time zone with : to necessary precision (e.g., -04, +05:30)
%Zalphabetic time zone abbreviation (e.g., EDT)

There's even more formatting available, but go read the man page if you need that much detail.

Time Trivia and/or Humour

Wikipedia says:

  • "Because [Unix time] does not handle leap seconds, it is neither a linear representation of time nor a true representation of UTC."
  • "Unix enthusiasts have a history of holding 'time_t parties' to celebrate significant values of the Unix time number."
  • "At 03:14:08 UTC on Tuesday, 19 January 2038, 32-bit versions of the Unix time stamp will cease to work, as it will overflow the largest value that can be held in a signed 32-bit number."

Bibliography