Linux – Working with date ranges in shell scripts

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

Leave a Reply

Your email address will not be published. Required fields are marked *