Date arithmetic in Unix shell scripts
Here is an easy way for doing date computations in shell scripting.
meetingDate='12/31/2011' # MM/DD/YYYY FormatreminderDate=`date --date=$meetingDate'-1 day' +'%m/%d/%Y'`echo $reminderDate
Below are more variations of date computation that can be achieved using date
utility.http://www.cyberciti.biz/tips/linux-unix-get-yesterdays-tomorrows-date.htmlhttp://www.cyberciti.biz/faq/linux-unix-formatting-dates-for-display/
This worked for me on RHEL.
I have written a bash script for converting dates expressed in English into conventionalmm/dd/yyyy dates. It is called ComputeDate.
Here are some examples of its use. For brevity I have placed the output of each invocationon the same line as the invocation, separarted by a colon (:). The quotes shown below are not necessary when running ComputeDate:
$ ComputeDate 'yesterday': 03/19/2010$ ComputeDate 'yes': 03/19/2010$ ComputeDate 'today': 03/20/2010$ ComputeDate 'tod': 03/20/2010$ ComputeDate 'now': 03/20/2010$ ComputeDate 'tomorrow': 03/21/2010$ ComputeDate 'tom': 03/21/2010$ ComputeDate '10/29/32': 10/29/2032$ ComputeDate 'October 29': 10/1/2029$ ComputeDate 'October 29, 2010': 10/29/2010$ ComputeDate 'this monday': 'this monday' has passed. Did you mean 'next monday?'$ ComputeDate 'a week after today': 03/27/2010$ ComputeDate 'this satu': 03/20/2010$ ComputeDate 'next monday': 03/22/2010$ ComputeDate 'next thur': 03/25/2010$ ComputeDate 'mon in 2 weeks': 03/28/2010$ ComputeDate 'the last day of the month': 03/31/2010$ ComputeDate 'the last day of feb': 2/28/2010$ ComputeDate 'the last day of feb 2000': 2/29/2000$ ComputeDate '1 week from yesterday': 03/26/2010$ ComputeDate '1 week from today': 03/27/2010$ ComputeDate '1 week from tomorrow': 03/28/2010$ ComputeDate '2 weeks from yesterday': 4/2/2010$ ComputeDate '2 weeks from today': 4/3/2010$ ComputeDate '2 weeks from tomorrow': 4/4/2010$ ComputeDate '1 week after the last day of march': 4/7/2010$ ComputeDate '1 week after next Thursday': 4/1/2010$ ComputeDate '2 weeks after the last day of march': 4/14/2010$ ComputeDate '2 weeks after 1 day after the last day of march': 4/15/2010$ ComputeDate '1 day after the last day of march': 4/1/2010$ ComputeDate '1 day after 1 day after 1 day after 1 day after today': 03/24/2010
I have included this script as an answer to this problem because it illustrates howto do date arithmetic via a set of bash functions and these functions may prove usefulfor others. It handles leap years and leap centuries correctly:
#! /bin/bash# ConvertDate -- convert a human-readable date to a MM/DD/YY date## Date ::= Month/Day/Year# | Month/Day# | DayOfWeek# | [this|next] DayOfWeek# | DayofWeek [of|in] [Number|next] weeks[s]# | Number [day|week][s] from Date# | the last day of the month# | the last day of Month## Month ::= January | February | March | April | May | ... | December# January ::= jan | january | 1# February ::= feb | january | 2# ...# December ::= dec | december | 12# Day ::= 1 | 2 | ... | 31# DayOfWeek ::= today | Sunday | Monday | Tuesday | ... | Saturday# Sunday ::= sun*# ...# Saturday ::= sat*## Number ::= Day | a## Author: Larry Morellif [ $# = 0 ]; then printdirections $0 exitfi# Request the value of a variableGetVar () { Var=$1 echo -n "$Var= [${!Var}]: " local X read X if [ ! -z $X ]; then eval $Var="$X" fi}IsLeapYear () { local Year=$1 if [ $[20$Year % 4] -eq 0 ]; then echo yes else echo no fi}# AddToDate -- compute another date within the same yearDayNames=(mon tue wed thu fri sat sun ) # To correspond with 'date' outputDay2Int () { ErrorFlag= case $1 in -e ) ErrorFlag=-e; shift ;; esac local dow=$1 n=0 while [ $n -lt 7 -a $dow != "${DayNames[n]}" ]; do let n++ done if [ -z "$ErrorFlag" -a $n -eq 7 ]; then echo Cannot convert $dow to a numeric day of wee exit fi echo $[n+1]}Months=(31 28 31 30 31 30 31 31 30 31 30 31)MonthNames=(jan feb mar apr may jun jul aug sep oct nov dec)# Returns the month (1-12) from a date, or a month nameMonth2Int () { ErrorFlag= case $1 in -e ) ErrorFlag=-e; shift ;; esac M=$1 Month=${M%%/*} # Remove /... case $Month in [a-z]* ) Month=${Month:0:3} M=0 while [ $M -lt 12 -a ${MonthNames[M]} != $Month ]; do let M++ done let M++ esac if [ -z "$ErrorFlag" -a $M -gt 12 ]; then echo "'$Month' Is not a valid month." exit fi echo $M}# Retrieve month,day,year from a legal dateGetMonth() { echo ${1%%/*}}GetDay() { echo $1 | col / 2}GetYear() { echo ${1##*/}}AddToDate() { local Date=$1 local days=$2 local Month=`GetMonth $Date` local Day=`echo $Date | col / 2` # Day of Date local Year=`echo $Date | col / 3` # Year of Date local LeapYear=`IsLeapYear $Year` if [ $LeapYear = "yes" ]; then let Months[1]++ fi Day=$[Day+days] while [ $Day -gt ${Months[$Month-1]} ]; do Day=$[Day - ${Months[$Month-1]}] let Month++ done echo "$Month/$Day/$Year"}# Convert a date to normal formNormalizeDate () { Date=`echo "$*" | sed 'sX *X/Xg'` local Day=`date +%d` local Month=`date +%m` local Year=`date +%Y` #echo Normalizing Date=$Date > /dev/tty case $Date in */*/* ) Month=`echo $Date | col / 1 ` Month=`Month2Int $Month` Day=`echo $Date | col / 2` Year=`echo $Date | col / 3` ;; */* ) Month=`echo $Date | col / 1 ` Month=`Month2Int $Month` Day=1 Year=`echo $Date | col / 2 ` ;; [a-z]* ) # Better be a month or day of week Exp=${Date:0:3} case $Exp in jan|feb|mar|apr|may|june|jul|aug|sep|oct|nov|dec ) Month=$Exp Month=`Month2Int $Month` Day=1 #Year stays the same ;; mon|tue|wed|thu|fri|sat|sun ) # Compute the next such day local DayOfWeek=`date +%u` D=`Day2Int $Exp` if [ $DayOfWeek -le $D ]; then Date=`AddToDate $Month/$Day/$Year $[D-DayOfWeek]` else Date=`AddToDate $Month/$Day/$Year $[7+D-DayOfWeek]` fi # Reset Month/Day/Year Month=`echo $Date | col / 1 ` Day=`echo $Date | col / 2` Year=`echo $Date | col / 3` ;; * ) echo "$Exp is not a valid month or day" exit ;; esac ;; * ) echo "$Date is not a valid date" exit ;; esac case $Day in [0-9]* );; # Day must be numeric * ) echo "$Date is not a valid date" exit ;; esac [0-9][0-9][0-9][0-9] );; # Year must be 4 digits [0-9][0-9] ) Year=20$Year ;; esac Date=$Month/$Day/$Year echo $Date}# NormalizeDate jan# NormalizeDate january# NormalizeDate jan 2009# NormalizeDate jan 22 1983# NormalizeDate 1/22# NormalizeDate 1 22# NormalizeDate sat# NormalizeDate sun# NormalizeDate monComputeExtension () { local Date=$1; shift local Month=`GetMonth $Date` local Day=`echo $Date | col / 2` local Year=`echo $Date | col / 3` local ExtensionExp="$*" case $ExtensionExp in *w*d* ) # like 5 weeks 3 days or even 5w2d ExtensionExp=`echo $ExtensionExp | sed 's/[a-z]/ /g'` weeks=`echo $ExtensionExp | col 1` days=`echo $ExtensionExp | col 2` days=$[7*weeks+days] Due=`AddToDate $Month/$Day/$Year $days` ;; *d ) # Like 5 days or 5d ExtensionExp=`echo $ExtensionExp | sed 's/[a-z]/ /g'` days=$ExtensionExp Due=`AddToDate $Month/$Day/$Year $days` ;; * ) Due=$ExtensionExp ;; esac echo $Due}# Pop -- remove the first element from an array and shift leftPop () { Var=$1 eval "unset $Var[0]" eval "$Var=(\${$Var[*]})"}ComputeDate () { local Date=`NormalizeDate $1`; shift local Expression=`echo $* | sed 's/^ *a /1 /;s/,/ /' | tr A-Z a-z ` local Exp=(`echo $Expression `) local Token=$Exp # first one local Ans= #echo "Computing date for ${Exp[*]}" > /dev/tty case $Token in */* ) # Regular date M=`GetMonth $Token` D=`GetDay $Token` Y=`GetYear $Token` if [ -z "$Y" ]; then Y=$Year elif [ ${#Y} -eq 2 ]; then Y=20$Y fi Ans="$M/$D/$Y" ;; yes* ) Ans=`AddToDate $Date -1` ;; tod*|now ) Ans=$Date ;; tom* ) Ans=`AddToDate $Date 1` ;; the ) case $Expression in *day*after* ) #the day after Date Pop Exp; # Skip the Pop Exp; # Skip day Pop Exp; # Skip after #echo Calling ComputeDate $Date ${Exp[*]} > /dev/tty Date=`ComputeDate $Date ${Exp[*]}` #Recursive call #echo "New date is " $Date > /dev/tty Ans=`AddToDate $Date 1` ;; *last*day*of*th*month|*end*of*th*month ) M=`date +%m` Day=${Months[M-1]} if [ $M -eq 2 -a `IsLeapYear $Year` = yes ]; then let Day++ fi Ans=$Month/$Day/$Year ;; *last*day*of* ) D=${Expression##*of } D=`NormalizeDate $D` M=`GetMonth $D` Y=`GetYear $D` # echo M is $M > /dev/tty Day=${Months[M-1]} if [ $M -eq 2 -a `IsLeapYear $Y` = yes ]; then let Day++ fi Ans=$[M]/$Day/$Y ;; * ) echo "Unknown expression: " $Expression exit ;; esac ;; next* ) # next DayOfWeek Pop Exp dow=`Day2Int $DayOfWeek` # First 3 chars tdow=`Day2Int ${Exp:0:3}` # First 3 chars n=$[7-dow+tdow] Ans=`AddToDate $Date $n` ;; this* ) Pop Exp dow=`Day2Int $DayOfWeek` tdow=`Day2Int ${Exp:0:3}` # First 3 chars if [ $dow -gt $tdow ]; then echo "'this $Exp' has passed. Did you mean 'next $Exp?'" exit fi n=$[tdow-dow] Ans=`AddToDate $Date $n` ;; [a-z]* ) # DayOfWeek ... M=${Exp:0:3} case $M in jan|feb|mar|apr|may|june|jul|aug|sep|oct|nov|dec ) ND=`NormalizeDate ${Exp[*]}` Ans=$ND ;; mon|tue|wed|thu|fri|sat|sun ) dow=`Day2Int $DayOfWeek` Ans=`NormalizeDate $Exp` if [ ${#Exp[*]} -gt 1 ]; then # Just a DayOfWeek #tdow=`GetDay $Exp` # First 3 chars #if [ $dow -gt $tdow ]; then #echo "'this $Exp' has passed. Did you mean 'next $Exp'?" #exit #fi #n=$[tdow-dow] #else # DayOfWeek in a future week Pop Exp # toss monday Pop Exp # toss in/off if [ $Exp = next ]; then Exp=2 fi n=$[7*(Exp-1)] # number of weeks n=$[n+7-dow+tdow] Ans=`AddToDate $Date $n` fi ;; esac ;; [0-9]* ) # Number weeks [from|after] Date n=$Exp Pop Exp; case $Exp in w* ) let n=7*n;; esac Pop Exp; Pop Exp #echo Calling ComputeDate $Date ${Exp[*]} > /dev/tty Date=`ComputeDate $Date ${Exp[*]}` #Recursive call #echo "New date is " $Date > /dev/tty Ans=`AddToDate $Date $n` ;; esac echo $Ans}Year=`date +%Y`Month=`date +%m`Day=`date +%d`DayOfWeek=`date +%a |tr A-Z a-z`Date="$Month/$Day/$Year"ComputeDate $Date $*
This script makes extensive use of another script I wrote (called col ... many apologies to those who use the standard col supplied with Linux). This version ofcol simplifies extracting columns from the stdin. Thus,
$ echo a b c d e | col 5 3 2
prints
e c b
Here it the col script:
#!/bin/sh# col -- extract columns from a file# Usage:# col [-r] [c] col-1 col-2 ...# where [c] if supplied defines the field separator# where each col-i represents a column interpreted according to the presence of -r as follows:# -r present : counting starts from the right end of the line# -r absent : counting starts from the left side of the lineSeparator=" "Reverse=falsecase "$1" in -r ) Reverse=true; shift; ;; [0-9]* ) ;; * )Separator="$1"; shift; ;;esaccase "$1" in -r ) Reverse=true; shift; ;; [0-9]* ) ;; * )Separator="$1"; shift; ;;esac# Replace each col-i with $iCols=""for f in $*do if [ $Reverse = true ]; then Cols="$Cols \$(NF-$f+1)," else Cols="$Cols \$$f," fidoneCols=`echo "$Cols" | sed 's/,$//'`#echo "Using column specifications of $Cols"awk -F "$Separator" "{print $Cols}"
It also uses printdirections for printing out directions when the script is invoked improperly:
#!/bin/sh## printdirections -- print header lines of a shell script## Usage:# printdirections path# where# path is a *full* path to the shell script in question# beginning with '/'## To use printdirections, you must include (as comments at the top# of your shell script) documentation for running the shell script.if [ $# -eq 0 -o "$*" = "-h" ]; then printdirections $0 exitfi# Delete the command invocation at the top of the file, if any# Delete from the place where printdirections occurs to the end of the file# Remove the # comments# There is a bizarre oddity here. sed '/#!/d;/.*printdirections/,$d;/ *#/!d;s/# //;s/#//' $1 > /tmp/printdirections.$$# Count the number of linesnumlines=`wc -l /tmp/printdirections.$$ | awk '{print $1}'`# Remove the last linenumlines=`expr $numlines - 1`head -n $numlines /tmp/printdirections.$$rm /tmp/printdirections.$$
To use this place the three scripts in the files ComputeDate, col, and printdirections, respectively. Place the file in directory named by your PATH, typically, ~/bin. Then make them executable with:
$ chmod a+x ComputeDate col printdirections
Problems? Send me some emaiL: morell AT cs.atu.edu Place ComputeDate in the subject.