date(1) Superpowers

November 22, 2019

Image my surprise when I found a Unix command I’ve been using for years had hidden superpowers - date(1).

Aside from a few times when I’ve had to covert seconds in the Unix Epoch back to a real date and time, I’ve not really thought much about it. But I’ve recently come into a situation where I need to do a lot with dates, so I started reading and experimenting. Much to my surprise, there’s quite a lot going on under the hood with date(1).

In this blog entry we’re going to look a quite a few features of date(1). Hop in and close the door while you read the manual And remember, this is date(1) on FreeBSD, not Linux or Windows.

Let’s start with the simple date command:

$ date
Thu Nov 21 12:40:26 EST 2019

Nothing new to see here. As we will move along quickly in the following examples you should always add the -j option. That way you won’t accidently set the date on your system.

Add a simple output format by specifying just the year:

$ date -j "+%Y"
2019

Add some more detail to the output format:

$ date -j "+%Y-%m-%d"
2019-11-21

Add the time as well:

$ date -j "+%Y-%m-%d %H:%M:%S"
2019-11-21 12:42:42

Use a US centric format and switch the date delimiters to ‘/’:

$ date -j "+%m/%d/%Y %H:%M:%S"
11/21/2019 12:44:27

Output the date as seconds in the Unix Epoch:

$ date -j "+%s"
1574358472

Now take a date input. This is what the -f option is for - parsing the date input format. Note that the -f option does not require a ‘+’ in the input specification.

Output a Unix standard date Use a text string for date input :

$ date -j -f "%a %b %d %T %Z %Y" "Thu Nov 21 12:49:42 EST 2019"
Thu Nov 21 12:49:42 EST 2019

Note that if you try to fool the date command it will try hard to figure out what you really want:

$ date -j -f "%a %b %d %T %Z %Y" "Fri Nov 21 12:49:42 EST 2019"
Thu Nov 21 12:49:42 EST 2019

$ date -j -f "%a %b %d %T %Z %Y" "Wed Sep 23 12:49:42 EST 2019"
Mon Sep 23 13:49:42 EDT 2019

$ date -j -f "%a %b %d %T %Z %Y" "Thu Nov 25 12:49:42 EST 2019"
Mon Nov 25 12:49:42 EST 2019

But some dates are unfortunately beyond date(1)’s reach:

$ date -j -f "%a %b %d %T %Z %Y" "Thu July 4 12:49:42 EST 1776"
date: nonexistent time

If you have fat fingers, date(1) might be able to help you:

$ date -j -f "%a %b %d %T %Z %Y" "Thu July 4 12:49:42 EST 20019"
Warning: Ignoring 1 extraneous characters in date string (9)

date(1) can also display dates as a relative offset to a known date using one or more -v options:

Today is

$ date
Thu Nov 21 18:30:00 EST 2019

Now show the date from yesterday:

$ date -j -v-1d
Wed Nov 20 18:30:54 EST 2019

Show the date from two years and 15 minutes ago:

$ date -j -v-2y -v-15M
Tue Nov 21 18:17:02 EST 2017

Or peer into the future:

$ date
Sat Nov 23 12:24:14 EST 2019

$ date -j -v+5y -v+6m
Fri May 23 12:24:16 EDT 2025

In many applications keeping a long list of events sorted by date can be tricky, unless you order the dates lexicographically which just happens to also work numerically. Consider a camera system that stores video clips by date:

20191117164540-CAM101-00155.mkv
20191117164603-CAM101-00156.mkv
20191117164816-CAM101-00157.mkv
20191117165316-CAM101-00158.mkv
20191117165816-CAM101-00159.mkv
20191117170316-CAM101-00160.mkv
20191117170816-CAM101-00161.mkv
20191117171316-CAM101-00162.mkv
20191117171816-CAM101-00163.mkv
 ...

In the lexicographic output usually produced by ls(1), the oldest file will always be at the top, and the newest file will always be at the bottom.

The output format spec for this date is “+%Y%m%d%H%M%S” as in:

$ date -j "+%Y%m%d%H%M%S"
20191121183842

The relative offset modifiers used above, also work;

$ date
Thu Nov 21 19:14:32 EST 2019

$ date -j -v-1d -v-2H  "+%Y%m%d%H%M%S"
20191120171435

Putting the input format (-f), a text date string, and the relative date modifiers together we have:

$  date -j  -v-3d -v-2H  -f "%a %b %d %T %Z %Y" "Sat Oct 19 12:49:42 EST 2019"
Wed Oct 16 11:49:42 EDT 2019

Use the US version and also output the date in lexicographic format. The output format spec comes last in this example:

$  date -j  -v-3d -v-2H  -f "%m/%d/%Y %H:%M:%S" "10/19/2019 12:49:30" "+%Y%m%d%H%M%S"
20191016104930

Using a text string is not optimal. We really want the actual date and time to come directly from the date(1) command. We do this through command substitution using the backtick operator (`).

The basic example, similar to the example in the date(1) man page, is:

$ date -j -f "%a %b %d %T %Z %Y" "`date`" 
Thu Nov 21 19:32:31 EST 2019

Layer in the relative offset format:

$ date
Thu Nov 21 19:34:03 EST 2019

$ date -j -v-1d -v-2H -f "%a %b %d %T %Z %Y" "`date`" 
Wed Nov 20 17:34:06 EST 2019

Now layer in the US input format, a relative offset, and output the lexicographic format:

$ date
Thu Nov 21 19:39:00 EST 2019

$ date -j  -v-1d -v-5H -v-15M  -f "%m/%d/%Y %H:%M:%S"  "`date -j "+%m/%d/%Y %H:%M:%S"`" "+%Y%m%d%H%M%S"
20191120142402 

Or use the same input format, but output the seconds in the Unix Epoch:

$ date -j  -v-1d -v-5H -v-15M -f "%m/%d/%Y %H:%M:%S"  "`date -j "+%m/%d/%Y %H:%M:%S"`" "+%s"
1574278014

Another novel use of date(1) is to convert the number of seconds in the Unix Epoch back into readable time. The -r option is used for this purpose. Here, we output the date in seconds of the Unix Epoch:

$ date -j "+%s"
1574383804

And convert it back again

$ date -r 1574383804
Thu Nov 21 19:50:04 EST 2019

or with a custom output:

$ date -r 1574383804  "+%m/%d/%Y %H:%M"
11/21/2019 19:50

Note that in the above example we did not specify seconds in the output format. This is true with date(1) generally - you can output what you want. However, if you are parsing input with the -f option and an input format spec, you must account for each piece of the date and time produced by the date(1) output.

Finally, some number fun:

$ date -r 0
Wed Dec 31 19:00:00 EST 1969

Wait, isn’t that supposed to be “midnight Jan 1, 1970”? Yes, and it is. It’s just output in EST, not UTC. You can set the timezone in a subshell:

$ (export TZ=UTC; date -r 0)
Thu Jan  1 00:00:00 UTC 1970

Need a random date sometime in the next 30,000 years? Try:

$ date -r `(export LC_ALL=C; dd if=/dev/random count=10 bs=1K status=none | tr -cd "[:number:]" ) | fold -w 12 | head -1`
Fri Oct 27 02:34:19 EDT 32615

$ date -r `(export LC_ALL=C; dd if=/dev/random count=10 bs=1K status=none | tr -cd "[:number:]" ) | fold -w 12 | head -1`
Sat Jan  2 23:13:52 EST 7492

$ date -r `(export LC_ALL=C; dd if=/dev/random count=10 bs=1K status=none | tr -cd "[:number:]" ) | fold -w 12 | head -1`
Sat Jul 13 04:10:57 EDT 26261

On my system, the maximum date I can convert with -r is:

$  date -r 67768036191694799
Wed Dec 31 23:59:59 EST 2147485547

The next second bombs out:

$  date -r 67768036191694800
date: invalid time

So I have about 2.1 billion years that I can successfully use date(1) on this machine.

The vaunted 32-bit time error problem in the year 2038 will occur at 03:14:07 on Tuesday, 19 Jan 2038. The Unix Epoch time value will be 2^31 - 1 or 2147483647. Systems that maintain time as a signed 32-bit integer will fail on the next second. You can see an intereting animation on the Year 2038 page on Wikipedia here.

Fortunately, date(1) on FreeBSD has already been modified to work correctly:

$ date -j -r 2147483647
Mon Jan 18 22:14:07 EST 2038

$ date -j -r 2147483648
Mon Jan 18 22:14:08 EST 2038

Superpowers indeed - enjoy!

–Jim B.