A recent request to write some shell scripts to manage a welter of dated, historical files on a Linux server gave me a chance for a little refresher on the power of the GNU style date command.
Both the BSD and GNU versions of the Unix/Linux date command have capabilities to do date arithmetic and spare a shell script hacker the need to worry about all those little details like the number of days in each month and which years are leap years. In some respects the natural-language-esque nature of the GNU version makes this wonderfully accessible:-
# date Fri Feb 12 18:48:43 GMT 2016 # date -d '2 weeks ago' Fri Jan 29 18:48:39 GMT 2016
Sometimes working out the right natural-language-esque can be a bit of a head-scratcher though…
Picky about my dates
If you’re unfamiliar with the difference between the BSD and GNU date commands you may well be unfamiliar with which one you’ve got on your system. A quick way to check is to issue a date –version:-
# date --version date (GNU coreutils) 8.4 Copyright (C) 2010 Free Software Foundation, Inc.
If you get a version back and anything about GNU or the Free Software Foundation then congratulations, you’ve got a GNU date command.
# date --version date: illegal option -- -
If, on the other hand you get an error then you ain’t got GNU date. Thanks for stopping by and happy Googling – GNU date is all I’m covering today!
Appearance is so important to me
The welter of files I need to manage all contain the date formatted as YYYYMMDD in the filename somewhere and my first challenge is to find it. Fortunately none of the filenames contain numbers elsewhere so a quick and simple sed will extract the relevant date for me by just stripping everything that isn’t a number:-
for file in * do FILE_DATE=`echo $file | sed 's/[^0-9]//g'` # … done
I’ll also need to get the same format from the date command. The display format is specified as a parameter starting with + and there are literally dozens of date components, styles and other formatting characters available to include. For my purposes a simple +%Y%m%d does the trick, giving me a four-digit year, two-digit month and two-digit day in that order
# date +%Y%m%d 20160212
Oh yeah! Gettin’ it every day of the week
The first range of dates I need to generate are the dates of the last few consecutive days. In a shell script I can use the seq command to generate a range of numbers and a simple -$i days bit of natural-language-esque in my date command will apply the offset:-
# for i in `seq 0 7`; do echo `date +%Y%m%d -d "-$i days"`; done 20160212 20160211 20160210 20160209 20160208 20160207 20160206 20160205
The sequence is zero based so includes today, since -0 days is today, as well as the previous seven dates.
If I wanted to generate a sequence of consecutive days starting back from an arbitrary date I could use the seq parameters to do this by simply adjusting the starting number. For example, seq 7 14 would give me the dates of all days in the previous week. However the date command will accept a ‘seed’ date in the –d option which can be a bit more versatile:-
# for i in `seq 0 7`; do echo `date +%Y%m%d -d "20160205 -$i days"`; done 20160205 20160204 20160203 20160202 20160201 20160131 20160130 20160129
Hey, once a week ain’t bad you know
Next up I need to generate the dates for the same day over the last few weeks (say, for example, the last 6 Mondays). To put it in natural-language-esque, Monday – (0-5) weeks:-
# for i in `seq 0 5`; do echo `date +%Y%m%d -d "monday - $i weeks"`; done 20160215 20160208 20160201 20160125 20160118 20160111
That’s given me six Mondays but there’s a gotcha here – the first is actually the date of next Monday, not the first previous Monday. We can ‘fix’ this by using last-monday instead:-
# for i in `seq 0 5`; do echo `date +%Y%m%d -d "last-monday - $i weeks"`; done 20160208 20160201 20160125 20160118 20160111 20160104
This correctly gives me the preceding six Mondays but there’s a gotcha here too – if I run this on a Monday the first Monday it throws out isn’t today but the previous Monday. How to handle this depends on your needs – for my purposes, since I’m merely triaging existing files, generating an extra future date that will never be matched isn’t a problem so the first option is good enough for me.
There’s another little wrinkle to watch for here too – older versions of the GNU date command don’t like the spaces that some developers (like me) usually put between operators and values. Run that last command on GNU date 4.5 for example and you won’t get an error but nor will you get the dates you expect:-
# for i in `seq 0 5`; do echo `date +%Y%m%d -d "last-monday - $i weeks"`; done 20160208 20160215 20160222 20160229 20160307 20160314
This has actually counted forwards. Removing the whitespace around the minus sign fixes this. Just a gentle reminder to test your date generation on your version of GNU date before using it to do something for real.
Lucky to be getting it once a month
My final requirement would be one of the gnarlier ones to shell script myself – I need the last day of each of the last few months.
Handling a fixed day over a series of months is fairly straightforward if we use date’s formatting capabilities to generate a seed date with a hard-coded component and then use that seed date in the calculation. For example, the first day of each of the last five months could be generated like this:-
# export START_DATE=`date +%Y%m01` # for i in `seq 0 4`; do echo `date +%Y%m%d -d "$START_DATE - $i months"`; done 20160201 20160101 20151201 20151101 20151001
Simple enough – we’ve generated a START_DATE based on the current date with a hard-coded 01 as the day of the month and then subtracted 0 – 4 months from it to get the first day of the previous five months.
But what about the last day of the prior months? Well, that would be one day behind the first day of the subsequent month – the range we’ve just generated. To put it in natural-language-esque – $i months – 1 day
# for i in `seq 0 4`; do echo `date +%Y%m%d -d "$START_DATE - $i months - 1 day"`; done 20160131 20151231 20151130 20151031 20150930
Again there’s a wrinkle to watch for with older versions of GNU date which aren’t too happy with subtraction from a seed date. Fortunately there’s a natural-language-esque alternative available – the ago keyword – which will do the equivalent thing:-
START_DATE=`date +%Y%m01` for i in `seq 0 4` do START_OF_MONTH=`date +%Y%m%d -d "$START_DATE $i month ago"` END_OF_PREVIOUS_MONTH=`date +%Y%m%d -d "$START_OF_MONTH 1 day ago"` echo $END_OF_PREVIOUS_MONTH done