163

How can two dates be compared in a shell?

Here is an example of how I would like to use this, though it does not work as-is:

todate=2013-07-18
cond=2013-07-15

if [ $todate -ge $cond ];
then
    break
fi                           

How can I achieve the desired result?

3
  • What do you want to loop for? Do you mean conditional rather than loop?
    – Mat
    Commented Jul 25, 2013 at 7:46
  • Why you tagged files? Are those dates actually file times?
    – manatwork
    Commented Jul 25, 2013 at 7:47
  • Check out this on stackoverflow: stackoverflow.com/questions/8116503/…
    – slm
    Commented Jul 25, 2013 at 14:04

15 Answers 15

213

The right answer is still missing:

todate=$(date -d 2013-07-18 +%s)
cond=$(date -d 2014-08-19 +%s)

if [ $todate -ge $cond ];
then
    break
fi  

Note that this requires GNU date.

  • The equivalent date syntax for BSD date (like found in OSX by default) is
    date -j -f "%F" 2014-08-19 +"%s"
    
  • If you want to compare with the current date, use $EPOCHSECONDS instead of one of the dates.
7
  • 16
    Donno why someone downvoted this, it's pretty accurate and doesn't deal with concatenating ugly strings. Convert your date to unix timestamp (in seconds), compare unix timestamps.
    – Nicholi
    Commented May 2, 2015 at 23:16
  • I think, me main advantage of this solution is, that you can do additions or subtractions easily. For example, I wanted to have a timeout of x seconds, but my first idea (timeout=$(`date +%Y%m%d%H%M%S` + 500)) is obviously wrong.
    – iGEL
    Commented Jul 20, 2018 at 9:59
  • 2
    You can include nanoseconds precision with date -d 2013-07-18 +%s%N
    – daparic
    Commented May 31, 2019 at 9:43
  • 5
    It's probably better to use (( ... > ... )) than [ ... -gt ... ] or similar. [[ "$x" -lt "$y" ]] returns true when $x is empty, in fact [[ "" -eq 0 ]] is TRUE. If something goes wrong populating $x and it winds up empty, I'd want "" < 1 to return false. (( "$x" < "$y" )) properly errors when $x is empty without needing extra checks. Commented Mar 11, 2021 at 0:39
  • @Nicholi it's downvoted because it doesn't work, I get an error saying command not found. ¯\_(ツ)_/¯ Commented Feb 14, 2023 at 15:26
67

Can use a ksh-style string comparison to compare the chronological ordering [of strings in a year, month, day format].

date_a=2013-07-18
date_b=2013-07-15

if [[ "$date_a" > "$date_b" ]] ;
then
    echo "break"
fi

Thankfully, when [strings that use the YYYY-MM-DD format] are sorted* in alphabetical order, they are also sorted* in chronological order.

(* - sorted or compared)

Nothing fancy needed in this case. yay!

10
  • 2
    This is far and away the smartest answer on here
    – Ed Randall
    Commented Feb 23, 2019 at 11:34
  • 2
    Use this answer, it is far more robust as it does not rely on an external command.
    – BitByteDog
    Commented Jun 13, 2020 at 22:20
  • 1
    That will not work in POSIX shell:
    – sena
    Commented May 18, 2022 at 12:47
  • 1
    @sena You're right it won't. So just use [ ] instead of [[ ]]. Commented Jul 13, 2022 at 17:10
  • 1
    @TrippKinetics I just realized that you can also quote the >, so [ 2023-02-15 \> 2023-01-01 ] && echo ok works. Commented Feb 15, 2023 at 4:20
41

You are missing the date format for the comparison:

#!/bin/bash

todate=$(date -d 2013-07-18 +"%Y%m%d")  # = 20130718
cond=$(date -d 2013-07-15 +"%Y%m%d")    # = 20130715

if [ $todate -ge $cond ]; #put the loop where you need it
then
 echo 'yes';
fi

You are missing looping structures too, how are you planning to get more dates?

3
  • This doesn't work with the date shipped with macOS.
    – Flimm
    Commented Dec 8, 2016 at 9:49
  • 1
    date -d is non standard and does not work on a typical UNIX. On BSD it even attempts to set the kenel value DST.
    – schily
    Commented Dec 7, 2018 at 10:13
  • 1
    The above works if you use gdate on macos, it's available in brew.
    – slm
    Commented Feb 11, 2020 at 7:37
10

This is not a problem of looping structures but of data types.

Those dates (todate and cond) are strings, not numbers, so you cannot use the "-ge" operator of test. (Remember that square bracket notation is equivalent to the command test.)

What you can do is use a different notation for your dates so that they are integers. For example:

date +%Y%m%d

will produce an integer like 20130715 for July 15th, 2013. Then you can compare your dates with "-ge" and equivalent operators.

Update: if your dates are given (e.g. you are reading them from a file in 2013-07-13 format) then you can preprocess them easily with tr.

$ echo "2013-07-15" | tr -d "-"
20130715
8

The operator -ge only works with integers, which your dates aren't.

If your script is a bash or ksh or zsh script, you can use the < operator instead. This operator is not available in dash or other shells that don't go much beyond the POSIX standard.

if [[ $cond < $todate ]]; then break; fi

In any shell, you can convert the strings to numbers while respecting the order of dates simply by removing the dashes.

if [ "$(echo "$todate" | tr -d -)" -ge "$(echo "$cond" | tr -d -)" ]; then break; fi

Alternatively, you can go traditional and use the expr utility.

if expr "$todate" ">=" "$cond" > /dev/null; then break; fi

As invoking subprocesses in a loop can be slow, you may prefer to do the transformation using shell string processing constructs.

todate_num=${todate%%-*}${todate#*-}; todate_num=${todate_num%%-*}${todate_num#*-}
cond_num=${cond%%-*}${cond#*-}; cond_num=${cond_num%%-*}${cond_num#*-}
if [ "$todate_num" -ge "$cond_num" ]; then break; fi

Of course, if you can retrieve the dates without the hyphens in the first place, you'll be able to compare them with -ge.

2
  • Where is the else branch in this bash? Commented Dec 15, 2016 at 14:12
  • 2
    This seems to be the most correct answer. Very helpful! Commented Oct 21, 2018 at 0:50
2

There is also this method from the article titled: Simple date and time calulation in BASH from unix.com.

These functions are an excerpt from a script in that thread!

date2stamp () {
    date --utc --date "$1" +%s
}

dateDiff (){
    case $1 in
        -s)   sec=1;      shift;;
        -m)   sec=60;     shift;;
        -h)   sec=3600;   shift;;
        -d)   sec=86400;  shift;;
        *)    sec=86400;;
    esac
    dte1=$(date2stamp $1)
    dte2=$(date2stamp $2)
    diffSec=$((dte2-dte1))
    if ((diffSec < 0)); then abs=-1; else abs=1; fi
    echo $((diffSec/sec*abs))
}

Usage

# calculate the number of days between 2 dates
    # -s in sec. | -m in min. | -h in hours  | -d in days (default)
    dateDiff -s "2006-10-01" "2006-10-31"
    dateDiff -m "2006-10-01" "2006-10-31"
    dateDiff -h "2006-10-01" "2006-10-31"
    dateDiff -d "2006-10-01" "2006-10-31"
    dateDiff  "2006-10-01" "2006-10-31"
3
  • 2
    This doesn't work with the date that ships with macOS.
    – Flimm
    Commented Dec 8, 2016 at 9:51
  • If you install gdate on MacOS via brew this can be easily modified to work by changing date to gdate.
    – slm
    Commented Oct 8, 2018 at 20:40
  • 1
    This answer was very helpful on my case. It should rate up! Commented Oct 21, 2018 at 0:40
1

I convert the Strings into unix-timestamps (seconds since 1.1.1970 0:0:0). These can compared easily

unix_todate=$(date -d "${todate}" "+%s")
unix_cond=$(date -d "${cond}" "+%s")
if [ ${unix_todate} -ge ${unix_cond} ]; then
   echo "over condition"
fi
2
1

specific answer

As I like to reduce forks and do permit a lot of tricks, there is my purpose:

todate=2013-07-18
cond=2013-07-15

Well, now:

{ read todate; read cond ;} < <(date -f - +%s <<<"$todate"$'\n'"$cond")

This will re-populate both variables $todate and $cond, using only one fork, with ouptut of date -f - wich take stdio for reading one date by line.

Finally, you could break your loop with

((todate>=cond))&&break

Or as a function:

myfunc() {
    local todate cond
    { read todate
      read cond
    } < <(
      date -f - +%s <<<"$1"$'\n'"$2"
    )
    ((todate>=cond))&&return
    printf "%(%a %d %b %Y)T older than %(%a %d %b %Y)T...\n" $todate $cond
}

Using 's builtin printf wich could render date time with seconds from epoch (see man bash;-)

This script only use one fork.

Alternative with limited forks and date reader function

This will create a dedicated subprocess (only one fork):

mkfifo /tmp/fifo
exec 99> >(exec stdbuf -i 0 -o 0 date -f - +%s >/tmp/fifo 2>&1)
exec 98</tmp/fifo
rm /tmp/fifo

As input and output are open, fifo entry could be deleted.

The function:

myDate() {
    local var="${@:$#}"
    shift
    echo >&99 "${@:1:$#-1}"
    read -t .01 -u 98 $var
}

Nota In order to prevent useless forks like todate=$(myDate 2013-07-18), the variable is to be set by the function himself. And to permit free syntax (with or without quotes to datestring), the variable name must be the last argument.

Then date comparission:

myDate 2013-07-18        todate
myDate Mon Jul 15 2013   cond
(( todate >= cond )) && {
    printf "To: %(%c)T > Cond: %(%c)T\n" $todate $cond
    break
}

may render:

To: Thu Jul 18 00:00:00 2013 > Cond: Mon Jul 15 00:00:00 2013
bash: break: only meaningful in a `for', `while', or `until' loop

if outside of a loop.

Or use shell-connector bash function:

wget https://github.com/F-Hauri/Connector-bash/raw/master/shell_connector.bash

or

wget https://f-hauri.ch/vrac/shell_connector.sh

(Wich are not exactly same: .sh do contain full test script if not sourced)

source shell_connector.sh
newConnector /bin/date '-f - +%s' @0 0

myDate 2013-07-18        todate
myDate "Mon Jul 15 2013"   cond
(( todate >= cond )) && {
    printf "To: %(%c)T > Cond: %(%c)T\n" $todate $cond
    break
}
1
1

In case you want to compare the dates of two files, you can use this:

if [ file1 -nt file2 ]
then
  echo "file1 is newer than file2"
fi
# or +ot for "older than"

See https://superuser.com/questions/188240/how-to-verify-that-file2-is-newer-than-file1-in-bash

0

dates are strings, not integers; you can't compare them with standard arithmetic operators.

one approach you can use, if the separator is guaranteed to be -:

IFS=-
read -ra todate <<<"$todate"
read -ra cond <<<"$cond"
for ((idx=0;idx<=numfields;idx++)); do
  (( todate[idx] > cond[idx] )) && break
done
unset IFS

This works as far back as bash 2.05b.0(1)-release.

0

You can also use mysql's builtin function to compare the dates. It gives the result in 'days'.

from_date="2015-01-02"
date_today="2015-03-10"
diff=$(mysql -u${DBUSER} -p${DBPASSWORD} -N -e"SELECT DATEDIFF('${date_today}','${from_date}');")

Just insert the values of $DBUSER and $DBPASSWORD variables according to yours.

0

FWIW: on Mac, it seems feeding just a date like "2017-05-05" into date will append the current time to the date and you will get different value each time you convert it to epoch time. to get a more consistent epoch time for fixed dates, include dummy values for hours, minutes, and seconds:

date -j -f "%Y-%m-%d %H:%M:%S" "2017-05-05 00:00:00" +"%s"

This may be a oddity limited to the version of date that shipped with macOS.... tested on macOS 10.12.6.

0

Echoing @Gilles answer ("The operator -ge only works with integers"), here is an example.

curr_date=$(date +'%Y-%m-%d')
echo "$curr_date"
2019-06-20

old_date="2019-06-19"
echo "$old_date"
2019-06-19

datetime integer conversions to epoch, per @mike-q's answer at https://stackoverflow.com/questions/10990949/convert-date-time-string-to-epoch-in-bash :

curr_date_int=$(date -d "${curr_date}" +"%s")
echo "$curr_date_int"
1561014000

old_date_int=$(date -d "${old_date}" +"%s")
echo "$old_date_int"
1560927600

"$curr_date" is greater than (more recent than) "$old_date", but this expression incorrectly evaluates as False:

if [[ "$curr_date" -ge "$old_date" ]]; then echo 'foo'; fi

... shown explicitly, here:

if [[ "$curr_date" -ge "$old_date" ]]; then echo 'foo'; else echo 'bar'; fi
bar

Integer comparisons:

if [[ "$curr_date_int" -ge "$old_date_int" ]]; then echo 'foo'; fi
foo

if [[ "$curr_date_int" -gt "$old_date_int" ]]; then echo 'foo'; fi
foo

if [[ "$curr_date_int" -lt "$old_date_int" ]]; then echo 'foo'; fi

if [[ "$curr_date_int" -lt "$old_date_int" ]]; then echo 'foo'; else echo 'bar'; fi
bar
0

The reason why this comparison doesn't work well with a hyphenated date string is the shell assumes a number with a leading 0 is an octal, in this case the month "07". There have been various solutions proposed but the quickest and easiest is to strip out the hyphens. Bash has a string substitution feature that makes this quick and easy then the comparison can be performed as an arithmetic expression:

todate=2013-07-18
cond=2013-07-15

if (( ${todate//-/} > ${cond//-/} ));
then
    echo larger
fi
0

It seems that all or most of the answers so far either assume you get to specify date format (which the question does not explicitly specify one way or the other), or assume that you have a feature rich version of date command or something.

Sometimes though, you are not in control of the date's format (e.g., the expiration date of an SSL certificate where date is given by a month name abbreviation) and you don't have access to a fancier date command. So while this solution is not completely general, it does run on a vanilla bash shell on Linux, Solaris, and FreeBSD, and handles month names (borrows significantly from a few smart answers from https://stackoverflow.com/questions/15252383/unix-convert-month-name-to-number):

#!/usr/bin/bash

function monthnumber {
    month=$(echo ${1:0:3} | tr '[a-z]' '[A-Z]')
    MONTHS="JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC"
    tmp=${MONTHS%%$month*}
    month=${#tmp}
    monthnumber=$((month/3+1))
    printf "%02d\n" $monthnumber
}

# Or at the expense of some flexibility and brevity, you get more readability:
function monthnumber2 {
    case $(echo ${1:0:3} | tr '[a-z]' '[A-Z]') in
        JAN) monthnumber="01" ;;
        FEB) monthnumber="02" ;;
        MAR) monthnumber="03" ;;
        APR) monthnumber="04" ;;
        MAY) monthnumber="05" ;;
        JUN) monthnumber="06" ;;
        JUL) monthnumber="07" ;;
        AUG) monthnumber="08" ;;
        SEP) monthnumber="09" ;;
        OCT) monthnumber="10" ;;
        NOV) monthnumber="11" ;;
        DEC) monthnumber="12" ;;
    esac
    printf "%02d\n" $monthnumber
}

TODAY=$( date "+%Y%m%d" )

echo "GET /" | openssl s_client -connect github.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > tmp.pem
cert_expiry_date=$(openssl x509 -in tmp.pem -noout -enddate | cut -d'=' -f2)
month_name=$(echo $cert_expiry_date | cut -d' ' -f1)
month_number=$( monthnumber $month_name )
cert_expiration_datestamp=$( echo $cert_expiry_date | awk "{printf \"%d%02d%02d\",\$4,\"${month_number}\",\$2}" )

echo "compare: [ $cert_expiration_datestamp -gt $TODAY ]"
if [ $cert_expiration_datestamp -gt $TODAY ] ; then
    echo "all ok, the cert expiration date is in the future."
else
    echo "WARNING: cert expiration date is in the past."
fi

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.