Date arithmetic in Unix shell scripts

前端 未结 14 1509
生来不讨喜
生来不讨喜 2020-11-27 22:29

I need to do date arithmetic in Unix shell scripts that I use to control the execution of third party programs.

I\'m using a function to increment a day and another

相关标签:
14条回答
  • 2020-11-27 22:35

    Here is an easy way for doing date computations in shell scripting.

    meetingDate='12/31/2011' # MM/DD/YYYY Format
    reminderDate=`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.html http://www.cyberciti.biz/faq/linux-unix-formatting-dates-for-display/

    This worked for me on RHEL.

    0 讨论(0)
  • 2020-11-27 22:38

    Looking into it further, I think you can simply use date. I've tried the following on OpenBSD: I took the date of Feb. 29th 2008 and a random hour (in the form of 080229301535) and added +1 to the day part, like so:

    $ date -j 0802301535
    Sat Mar  1 15:35:00 EST 2008
    

    As you can see, date formatted the time correctly...

    HTH

    0 讨论(0)
  • 2020-11-27 22:38

    The book "Shell Script Recipes: A Problem Solution Approach" (ISBN: 978-1-59059-471-1) by Chris F.A. Johnson has a date functions library that might be helpful. The source code is available at http://apress.com/book/downloadfile/2146 (the date functions are in Chapter08/data-funcs-sh within the tar file).

    0 讨论(0)
  • 2020-11-27 22:39

    Here are my two pennies worth - a script wrapper making use of date and grep.

    Example Usage

    > sh ./datecalc.sh "2012-08-04 19:43:00" + 1s
    2012-08-04 19:43:00 + 0d0h0m1s
    2012-08-04 19:43:01
    
    > sh ./datecalc.sh "2012-08-04 19:43:00" - 1s1m1h1d
    2012-08-04 19:43:00 - 1d1h1m1s
    2012-08-03 18:41:59
    
    > sh ./datecalc.sh "2012-08-04 19:43:00" - 1d2d1h2h1m2m1s2sblahblah
    2012-08-04 19:43:00 - 1d1h1m1s
    2012-08-03 18:41:59
    
    > sh ./datecalc.sh "2012-08-04 19:43:00" x 1d
    Bad operator :-(
    
    > sh ./datecalc.sh "2012-08-04 19:43:00"
    Missing arguments :-(
    
    > sh ./datecalc.sh gibberish + 1h
    date: invalid date `gibberish'
    Invalid date :-(
    

    Script

    #!/bin/sh
    
    # Usage:
    #
    # datecalc "<date>" <operator> <period>
    #
    # <date> ::= see "man date", section "DATE STRING"
    # <operator> ::= + | -
    # <period> ::= INTEGER<unit> | INTEGER<unit><period>
    # <unit> ::= s | m | h | d
    
    if [ $# -lt 3 ]; then
    echo "Missing arguments :-("
    exit; fi
    
    date=`eval "date -d \"$1\" +%s"`
    if [ -z $date ]; then
    echo "Invalid date :-("
    exit; fi
    
    if ! ([ $2 == "-" ] || [ $2 == "+" ]); then
    echo "Bad operator :-("
    exit; fi
    op=$2
    
    minute=$[60]
    hour=$[$minute*$minute]
    day=$[24*$hour]
    
    s=`echo $3 | grep -oe '[0-9]*s' | grep -m 1 -oe '[0-9]*'`
    m=`echo $3 | grep -oe '[0-9]*m' | grep -m 1 -oe '[0-9]*'`
    h=`echo $3 | grep -oe '[0-9]*h' | grep -m 1 -oe '[0-9]*'`
    d=`echo $3 | grep -oe '[0-9]*d' | grep -m 1 -oe '[0-9]*'`
    if [ -z $s ]; then s=0; fi
    if [ -z $m ]; then m=0; fi
    if [ -z $h ]; then h=0; fi
    if [ -z $d ]; then d=0; fi
    
    ms=$[$m*$minute]
    hs=$[$h*$hour]
    ds=$[$d*$day]
    
    sum=$[$s+$ms+$hs+$ds]
    out=$[$date$op$sum]
    formattedout=`eval "date -d @$out +\"%Y-%m-%d %H:%M:%S\""`
    
    echo $1 $2 $d"d"$h"h"$m"m"$s"s"
    echo $formattedout
    
    0 讨论(0)
  • 2020-11-27 22:41

    Why not write your scripts using a language like perl or python instead which more naturally supports complex date processing? Sure you can do it all in bash, but I think you will also get more consistency across platforms using python for example, so long as you can ensure that perl or python is installed.

    I should add that it is quite easy to wire in python and perl scripts into a containing shell script.

    0 讨论(0)
  • 2020-11-27 22:43

    I have written a bash script for converting dates expressed in English into conventional mm/dd/yyyy dates. It is called ComputeDate.

    Here are some examples of its use. For brevity I have placed the output of each invocation on 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 how to do date arithmetic via a set of bash functions and these functions may prove useful for 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 Morell
    
    if [ $# = 0 ]; then
       printdirections $0
       exit
    fi
    
    
    
    # Request the value of a variable
    GetVar () {
       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 year
    
    DayNames=(mon tue wed thu fri sat sun )  # To correspond with 'date' output
    
    Day2Int () {
       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 name
    Month2Int () {
       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 date
    GetMonth() {
       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 form
    NormalizeDate () {
       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 mon
    
    ComputeExtension () {
    
       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 left
    Pop () {
       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 of col 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 line
    Separator=" "
    Reverse=false
    case "$1" in
     -r )  Reverse=true; shift;
     ;;
     [0-9]* )
     ;;
     * )Separator="$1"; shift;
     ;;
    esac
    
    case "$1" in
     -r )  Reverse=true; shift;
     ;;
     [0-9]* )
     ;;
     * )Separator="$1"; shift;
     ;;
    esac
    
    #  Replace each col-i with $i
    Cols=""
    for  f in $*
    do
      if [ $Reverse = true ]; then
         Cols="$Cols \$(NF-$f+1),"
      else
         Cols="$Cols \$$f,"
      fi
    
    done
    
    Cols=`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
       exit
    fi
    #  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 lines
    numlines=`wc -l /tmp/printdirections.$$ | awk '{print $1}'`
    
    #  Remove the last   line
    numlines=`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.

    0 讨论(0)
提交回复
热议问题