How to calculate time elapsed in bash script?

BashDate

Bash Problem Overview


I print the start and end time using date +"%T", which results in something like:

10:33:56
10:36:10

How could I calculate and print the difference between these two?

I would like to get something like:

2m 14s

Bash Solutions


Solution 1 - Bash

Bash has a handy SECONDS builtin variable that tracks the number of seconds that have passed since the shell was started. This variable retains its properties when assigned to, and the value returned after the assignment is the number of seconds since the assignment plus the assigned value.

Thus, you can just set SECONDS to 0 before starting the timed event, simply read SECONDS after the event, and do the time arithmetic before displaying.

#!/usr/bin/env bash

SECONDS=0
# do some work
duration=$SECONDS
echo "$(($duration / 60)) minutes and $(($duration % 60)) seconds elapsed."

As this solution doesn't depend on date +%s (which is a GNU extension), it's portable to all systems supported by Bash.

Solution 2 - Bash

Seconds

To measure elapsed time (in seconds) we need:

  • an integer that represents the count of elapsed seconds and
  • a way to convert such integer to an usable format.

An integer value of elapsed seconds:
  • There are two bash internal ways to find an integer value for the number of elapsed seconds:
  1. Bash variable SECONDS (if SECONDS is unset it loses its special property).

    • Setting the value of SECONDS to 0:

            SECONDS=0
            sleep 1  # Process to execute
            elapsedseconds=$SECONDS
      
    • Storing the value of the variable SECONDS at the start:

            a=$SECONDS
            sleep 1  # Process to execute
            elapsedseconds=$(( SECONDS - a ))
      
  2. Bash printf option %(datefmt)T:

         a="$(TZ=UTC0 printf '%(%s)T\n' '-1')"    ### `-1`  is the current time
         sleep 1                                  ### Process to execute
         elapsedseconds=$(( $(TZ=UTC0 printf '%(%s)T\n' '-1') - a ))
    

Convert such integer to an usable format

The bash internal printf can do that directly:

$ TZ=UTC0 printf '%(%H:%M:%S)T\n' 12345
03:25:45

similarly

$ elapsedseconds=$((12*60+34))
$ TZ=UTC0 printf '%(%H:%M:%S)T\n' "$elapsedseconds"
00:12:34

but this will fail for durations of more than 24 hours, as we actually print a wallclock time, not really a duration:

$ hours=30;mins=12;secs=24
$ elapsedseconds=$(( ((($hours*60)+$mins)*60)+$secs ))
$ TZ=UTC0 printf '%(%H:%M:%S)T\n' "$elapsedseconds"
06:12:24

For the lovers of detail, from bash-hackers.org:

> %(FORMAT)T outputs the date-time string resulting from using FORMAT > as a format string for strftime(3). The associated argument is the > number of seconds since Epoch, or -1 (current time) or -2 (shell > startup time). If no corresponding argument is supplied, the current > time is used as default.

So you may want to just call textifyDuration $elpasedseconds where textifyDuration is yet another implementation of duration printing:

textifyDuration() {
   local duration=$1
   local shiff=$duration
   local secs=$((shiff % 60));  shiff=$((shiff / 60));
   local mins=$((shiff % 60));  shiff=$((shiff / 60));
   local hours=$shiff
   local splur; if [ $secs  -eq 1 ]; then splur=''; else splur='s'; fi
   local mplur; if [ $mins  -eq 1 ]; then mplur=''; else mplur='s'; fi
   local hplur; if [ $hours -eq 1 ]; then hplur=''; else hplur='s'; fi
   if [[ $hours -gt 0 ]]; then
      txt="$hours hour$hplur, $mins minute$mplur, $secs second$splur"
   elif [[ $mins -gt 0 ]]; then
      txt="$mins minute$mplur, $secs second$splur"
   else
      txt="$secs second$splur"
   fi
   echo "$txt (from $duration seconds)"
}

GNU date.

To get formated time we should use an external tool (GNU date) in several ways to get up to almost a year length and including Nanoseconds.

Math inside date.

There is no need for external arithmetic, do it all in one step inside date:

date -u -d "0 $FinalDate seconds - $StartDate seconds" +"%H:%M:%S"

Yes, there is a 0 zero in the command string. It is needed.

That's assuming you could change the date +"%T" command to a date +"%s" command so the values will be stored (printed) in seconds.

Note that the command is limited to:

  • Positive values of $StartDate and $FinalDate seconds.
  • The value in $FinalDate is bigger (later in time) than $StartDate.
  • Time difference smaller than 24 hours.
  • You accept an output format with Hours, Minutes and Seconds. Very easy to change.
  • It is acceptable to use -u UTC times. To avoid "DST" and local time corrections.

If you must use the 10:33:56 string, well, just convert it to seconds,
also, the word seconds could be abbreviated as sec:

string1="10:33:56"
string2="10:36:10"
StartDate=$(date -u -d "$string1" +"%s")
FinalDate=$(date -u -d "$string2" +"%s")
date -u -d "0 $FinalDate sec - $StartDate sec" +"%H:%M:%S"

Note that the seconds time conversion (as presented above) is relative to the start of "this" day (Today).


The concept could be extended to nanoseconds, like this:

string1="10:33:56.5400022"
string2="10:36:10.8800056"
StartDate=$(date -u -d "$string1" +"%s.%N")
FinalDate=$(date -u -d "$string2" +"%s.%N")
date -u -d "0 $FinalDate sec - $StartDate sec" +"%H:%M:%S.%N"

If is required to calculate longer (up to 364 days) time differences, we must use the start of (some) year as reference and the format value %j (the day number in the year):

Similar to:

string1="+10 days 10:33:56.5400022"
string2="+35 days 10:36:10.8800056"
StartDate=$(date -u -d "2000/1/1 $string1" +"%s.%N")
FinalDate=$(date -u -d "2000/1/1 $string2" +"%s.%N")
date -u -d "2000/1/1 $FinalDate sec - $StartDate sec" +"%j days %H:%M:%S.%N"

Output:
026 days 00:02:14.340003400

Sadly, in this case, we need to manually subtract 1 ONE from the number of days. The date command view the first day of the year as 1. Not that difficult ...

a=( $(date -u -d "2000/1/1 $FinalDate sec - $StartDate sec" +"%j days %H:%M:%S.%N") )
a[0]=$((10#${a[0]}-1)); echo "${a[@]}"



The use of long number of seconds is valid and documented here:
https://www.gnu.org/software/coreutils/manual/html_node/Examples-of-date.html#Examples-of-date


Busybox date

A tool used in smaller devices (a very small executable to install): Busybox.

Either make a link to busybox called date:

$ ln -s /bin/busybox date

Use it then by calling this date (place it in a PATH included directory).

Or make an alias like:

$ alias date='busybox date'

Busybox date has a nice option: -D to receive the format of the input time. That opens up a lot of formats to be used as time. Using the -D option we can convert the time 10:33:56 directly:

date -D "%H:%M:%S" -d "10:33:56" +"%Y.%m.%d-%H:%M:%S"

And as you can see from the output of the Command above, the day is assumed to be "today". To get the time starting on epoch:

$ string1="10:33:56"
$ date -u -D "%Y.%m.%d-%H:%M:%S" -d "1970.01.01-$string1" +"%Y.%m.%d-%H:%M:%S"
1970.01.01-10:33:56

Busybox date can even receive the time (in the format above) without -D:

$ date -u -d "1970.01.01-$string1" +"%Y.%m.%d-%H:%M:%S"
1970.01.01-10:33:56

And the output format could even be seconds since epoch.

$ date -u -d "1970.01.01-$string1" +"%s"
52436

For both times, and a little bash math (busybox can not do the math, yet):

string1="10:33:56"
string2="10:36:10"
t1=$(date -u -d "1970.01.01-$string1" +"%s")
t2=$(date -u -d "1970.01.01-$string2" +"%s")
echo $(( t2 - t1 ))

Or formatted:

$ date -u -D "%s" -d "$(( t2 - t1 ))" +"%H:%M:%S"
00:02:14

Solution 3 - Bash

Here is how I did it:

START=$(date +%s);
sleep 1; # Your stuff
END=$(date +%s);
echo $((END-START)) | awk '{print int($1/60)":"int($1%60)}'

Really simple, take the number of seconds at the start, then take the number of seconds at the end, and print the difference in minutes:seconds.

Solution 4 - Bash

Another option is to use datediff from dateutils (<http://www.fresse.org/dateutils/#datediff>;):

$ datediff 10:33:56 10:36:10
134s
$ datediff 10:33:56 10:36:10 -f%H:%M:%S
0:2:14
$ datediff 10:33:56 10:36:10 -f%0H:%0M:%0S
00:02:14

You could also use gawk. mawk 1.3.4 also has strftime and mktime but older versions of mawk and nawk don't.

$ TZ=UTC0 awk 'BEGIN{print strftime("%T",mktime("1970 1 1 10 36 10")-mktime("1970 1 1 10 33 56"))}'
00:02:14

Or here's another way to do it with GNU date:

$ date -ud@$(($(date -ud'1970-01-01 10:36:10' +%s)-$(date -ud'1970-01-01 10:33:56' +%s))) +%T
00:02:14

Solution 5 - Bash

I'd like to propose another way that avoid recalling date command. It may be helpful in case if you have already gathered timestamps in %T date format:

ts_get_sec()
{
  read -r h m s <<< $(echo $1 | tr ':' ' ' )
  echo $(((h*60*60)+(m*60)+s))
}

start_ts=10:33:56
stop_ts=10:36:10

START=$(ts_get_sec $start_ts)
STOP=$(ts_get_sec $stop_ts)
DIFF=$((STOP-START))

echo "$((DIFF/60))m $((DIFF%60))s"

we can even handle millisecondes in the same way.

ts_get_msec()
{
  read -r h m s ms <<< $(echo $1 | tr '.:' ' ' )
  echo $(((h*60*60*1000)+(m*60*1000)+(s*1000)+ms))
}

start_ts=10:33:56.104
stop_ts=10:36:10.102

START=$(ts_get_msec $start_ts)
STOP=$(ts_get_msec $stop_ts)
DIFF=$((STOP-START))

min=$((DIFF/(60*1000)))
sec=$(((DIFF%(60*1000))/1000))
ms=$(((DIFF%(60*1000))%1000))

echo "${min}:${sec}.$ms"

Solution 6 - Bash

Following on from Daniel Kamil Kozar's answer, to show hours/minutes/seconds:

echo "Duration: $(($DIFF / 3600 )) hours $((($DIFF % 3600) / 60)) minutes $(($DIFF % 60)) seconds"

So the full script would be:

date1=$(date +"%s")
date2=$(date +"%s")
DIFF=$(($date2-$date1))
echo "Duration: $(($DIFF / 3600 )) hours $((($DIFF % 3600) / 60)) minutes $(($DIFF % 60)) seconds"

Solution 7 - Bash

Here's some magic:

time1=14:30
time2=$( date +%H:%M ) # 16:00
diff=$(  echo "$time2 - $time1"  | sed 's%:%+(1/60)*%g' | bc -l )
echo $diff hours
# outputs 1.5 hours

sed replaces a : with a formula to convert to 1/60. Then the time calculation that is made by bc

Solution 8 - Bash

As of date (GNU coreutils) 7.4 you can now use -d to do arithmetic :

$ date -d -30days
Sat Jun 28 13:36:35 UTC 2014

$ date -d tomorrow
Tue Jul 29 13:40:55 UTC 2014

The units you can use are days, years, months, hours, minutes, and seconds :

$ date -d tomorrow+2days-10minutes
Thu Jul 31 13:33:02 UTC 2014

Solution 9 - Bash

% start=$(date +%s)
% echo "Diff: $(date -d @$(($(date +%s)-$start)) +"%M minutes %S seconds")"
Diff: 00 minutes 11 seconds

Solution 10 - Bash

Or wrap it up a bit

alias timerstart='starttime=$(date +"%s")'
alias timerstop='echo seconds=$(($(date +"%s")-$starttime))'

Then this works.

timerstart; sleep 2; timerstop
seconds=2

Solution 11 - Bash

date can give you the difference and format it for you (OS X options shown)

date -ujf%s $(($(date -jf%T "10:36:10" +%s) - $(date -jf%T "10:33:56" +%s))) +%T
# 00:02:14

date -ujf%s $(($(date -jf%T "10:36:10" +%s) - $(date -jf%T "10:33:56" +%s))) \
    +'%-Hh %-Mm %-Ss'
# 0h 2m 14s

Some string processing can remove those empty values

date -ujf%s $(($(date -jf%T "10:36:10" +%s) - $(date -jf%T "10:33:56" +%s))) \
    +'%-Hh %-Mm %-Ss' | sed "s/[[:<:]]0[hms] *//g"
# 2m 14s

This won't work if you place the earlier time first. If you need to handle that, change $(($(date ...) - $(date ...))) to $(echo $(date ...) - $(date ...) | bc | tr -d -)

Solution 12 - Bash

Here is a solution using only the date commands capabilities using "ago", and not using a second variable to store the finish time:

#!/bin/bash

# save the current time
start_time=$( date +%s.%N )

# tested program
sleep 1

# the current time after the program has finished
# minus the time when we started, in seconds.nanoseconds
elapsed_time=$( date +%s.%N --date="$start_time seconds ago" )

echo elapsed_time: $elapsed_time

this gives:

$ ./time_elapsed.sh 
elapsed_time: 1.002257120

Solution 13 - Bash

#!/bin/bash

START_TIME=$(date +%s)

sleep 4

echo "Total time elapsed: $(date -ud "@$(($(date +%s) - $START_TIME))" +%T) (HH:MM:SS)"
$ ./total_time_elapsed.sh 
Total time elapsed: 00:00:04 (HH:MM:SS)

Solution 14 - Bash

Generalizing @nisetama's solution using GNU date (trusty Ubuntu 14.04 LTS):

start=`date`
# <processing code>
stop=`date`
duration=`date -ud@$(($(date -ud"$stop" +%s)-$(date -ud"$start" +%s))) +%T`

echo $start
echo $stop
echo $duration

yielding:

Wed Feb 7 12:31:16 CST 2018
Wed Feb 7 12:32:25 CST 2018
00:01:09

Solution 15 - Bash

I needed a time difference script for use with mencoder (its --endpos is relative), and my solution is to call a Python script:

$ ./timediff.py 1:10:15 2:12:44
1:02:29

fractions of seconds are also supported:

$ echo "diff is `./timediff.py 10:51.6 12:44` (in hh:mm:ss format)"
diff is 0:01:52.4 (in hh:mm:ss format)

and it can tell you that the difference between 200 and 120 is 1h 20m:

$ ./timediff.py 120:0 200:0
1:20:0

and can convert any (probably fractional) number of seconds or minutes or hours to hh:mm:ss

$ ./timediff.py 0 3600
1:00:0
$ ./timediff.py 0 3.25:0:0
3:15:0

timediff.py:

#!/usr/bin/python

import sys

def x60(h,m):
    return 60*float(h)+float(m)

def seconds(time):
    try:
	   h,m,s = time.split(':')
	   return x60(x60(h,m),s)
    except ValueError:
	   try:
	      m,s = time.split(':')
	      return x60(m,s)
	   except ValueError:
	      return float(time)

def difftime(start, end):
    d = seconds(end) - seconds(start)
    print '%d:%02d:%s' % (d/3600,d/60%60,('%02f' % (d%60)).rstrip('0').rstrip('.'))

if __name__ == "__main__":
   difftime(sys.argv[1],sys.argv[2])

Solution 16 - Bash

With GNU units:

$ units
2411 units, 71 prefixes, 33 nonlinear units
You have: (10hr+36min+10s)-(10hr+33min+56s)
You want: s
	* 134
	/ 0.0074626866
You have: (10hr+36min+10s)-(10hr+33min+56s)
You want: min
	* 2.2333333
	/ 0.44776119

Solution 17 - Bash

Define this function (say in ~/.bashrc):

time::clock() {
    [ -z "$ts" ]&&{ ts=`date +%s%N`;return;}||te=`date +%s%N`
    printf "%6.4f" $(echo $((te-ts))/1000000000 | bc -l)
    unset ts te
}

Now you can measure time of parts of your scripts:

$ cat script.sh
# ... code ...
time::clock
sleep 0.5
echo "Total time: ${time::clock}"
# ... more code ...

$ ./script.sh
Total time: 0.5060

very useful to find execution bottlenecks.

Solution 18 - Bash

I realize this is an older post, but I came it across it today while working on a script that would take dates and times from a log file and compute the delta. The script below is certainly overkill, and I highly recommend checking my logic and maths.

#!/bin/bash

dTime=""
tmp=""

#firstEntry="$(head -n 1 "$LOG" | sed 's/.*] \([0-9: -]\+\).*/\1/')"
firstEntry="2013-01-16 01:56:37"
#lastEntry="$(tac "$LOG" | head -n 1 | sed 's/.*] \([0-9: -]\+\).*/\1/')"
lastEntry="2014-09-17 18:24:02"

# I like to make the variables easier to parse
firstEntry="${firstEntry//-/ }"
lastEntry="${lastEntry//-/ }"
firstEntry="${firstEntry//:/ }"
lastEntry="${lastEntry//:/ }"

# remove the following lines in production
echo "$lastEntry"
echo "$firstEntry"

# compute days in last entry
for i in `seq 1 $(echo $lastEntry|awk '{print $2}')`; do {
  case "$i" in
   1|3|5|7|8|10|12 )
    dTime=$(($dTime+31))
    ;;
   4|6|9|11 )
    dTime=$(($dTime+30))
    ;;
   2 )
    dTime=$(($dTime+28))
    ;;
  esac
} done

# do leap year calculations for all years between first and last entry
for i in `seq $(echo $firstEntry|awk '{print $1}') $(echo $lastEntry|awk '{print $1}')`; do {
  if [ $(($i%4)) -eq 0 ] && [ $(($i%100)) -eq 0 ] && [ $(($i%400)) -eq 0 ]; then {
    if [ "$i" = "$(echo $firstEntry|awk '{print $1}')" ] && [ $(echo $firstEntry|awk '{print $2}') -lt 2 ]; then {
      dTime=$(($dTime+1))
    } elif [ $(echo $firstEntry|awk '{print $2}') -eq 2 ] && [ $(echo $firstEntry|awk '{print $3}') -lt 29 ]; then {
      dTime=$(($dTime+1))
    } fi
  } elif [ $(($i%4)) -eq 0 ] && [ $(($i%100)) -ne 0 ]; then {
    if [ "$i" = "$(echo $lastEntry|awk '{print $1}')" ] && [ $(echo $lastEntry|awk '{print $2}') -gt 2 ]; then {
      dTime=$(($dTime+1))
    } elif [ $(echo $lastEntry|awk '{print $2}') -eq 2 ] && [ $(echo $lastEntry|awk '{print $3}') -ne 29 ]; then {
      dTime=$(($dTime+1))
    } fi
  } fi
} done

# substract days in first entry
for i in `seq 1 $(echo $firstEntry|awk '{print $2}')`; do {
  case "$i" in
   1|3|5|7|8|10|12 )
    dTime=$(($dTime-31))
    ;;
   4|6|9|11 )
    dTime=$(($dTime-30))
    ;;
   2 )
    dTime=$(($dTime-28))
    ;;
  esac
} done

dTime=$(($dTime+$(echo $lastEntry|awk '{print $3}')-$(echo $firstEntry|awk '{print $3}')))

# The above gives number of days for sample. Now we need hours, minutes, and seconds
# As a bit of hackery I just put the stuff in the best order for use in a for loop
dTime="$(($(echo $lastEntry|awk '{print $6}')-$(echo $firstEntry|awk '{print $6}'))) $(($(echo $lastEntry|awk '{print $5}')-$(echo $firstEntry|awk '{print $5}'))) $(($(echo $lastEntry|awk '{print $4}')-$(echo $firstEntry|awk '{print $4}'))) $dTime"
tmp=1
for i in $dTime; do {
  if [ $i -lt 0 ]; then {
    case "$tmp" in
     1 )
      tmp="$(($(echo $dTime|awk '{print $1}')+60)) $(($(echo $dTime|awk '{print $2}')-1))"
      dTime="$tmp $(echo $dTime|awk '{print $3" "$4}')"
      tmp=1
      ;;
     2 )
      tmp="$(($(echo $dTime|awk '{print $2}')+60)) $(($(echo $dTime|awk '{print $3}')-1))"
      dTime="$(echo $dTime|awk '{print $1}') $tmp $(echo $dTime|awk '{print $4}')"
      tmp=2
      ;;
     3 )
      tmp="$(($(echo $dTime|awk '{print $3}')+24)) $(($(echo $dTime|awk '{print $4}')-1))"
      dTime="$(echo $dTime|awk '{print $1" "$2}') $tmp"
      tmp=3
      ;;
    esac
  } fi
  tmp=$(($tmp+1))
} done

echo "The sample time is $(echo $dTime|awk '{print $4}') days, $(echo $dTime|awk '{print $3}') hours, $(echo $dTime|awk '{print $2}') minutes, and $(echo $dTime|awk '{print $1}') seconds."

You will get output as follows.

2012 10 16 01 56 37
2014 09 17 18 24 02
The sample time is 700 days, 16 hours, 27 minutes, and 25 seconds.

I modified the script a bit to make it standalone (ie. just set variable values), but maybe the general idea comes across as well. You'd might want some additional error checking for negative values.

Solution 19 - Bash

Here's my bash implementation (with bits taken from other SO ;-)

function countTimeDiff() {
    timeA=$1 # 09:59:35
    timeB=$2 # 17:32:55

    # feeding variables by using read and splitting with IFS
    IFS=: read ah am as <<< "$timeA"
    IFS=: read bh bm bs <<< "$timeB"

    # Convert hours to minutes.
    # The 10# is there to avoid errors with leading zeros
    # by telling bash that we use base 10
    secondsA=$((10#$ah*60*60 + 10#$am*60 + 10#$as))
    secondsB=$((10#$bh*60*60 + 10#$bm*60 + 10#$bs))
    DIFF_SEC=$((secondsB - secondsA))
    echo "The difference is $DIFF_SEC seconds.";

    SEC=$(($DIFF_SEC%60))
    MIN=$((($DIFF_SEC-$SEC)%3600/60))
    HRS=$((($DIFF_SEC-$MIN*60)/3600))
    TIME_DIFF="$HRS:$MIN:$SEC";
    echo $TIME_DIFF;
}

$ countTimeDiff 2:15:55 2:55:16
The difference is 2361 seconds.
0:39:21

Not tested, may be buggy.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMisha MoroshkoView Question on Stackoverflow
Solution 1 - BashDaniel Kamil KozarView Answer on Stackoverflow
Solution 2 - Bashuser2350426View Answer on Stackoverflow
Solution 3 - BashDorianView Answer on Stackoverflow
Solution 4 - BashnisetamaView Answer on Stackoverflow
Solution 5 - BashZskdanView Answer on Stackoverflow
Solution 6 - BashmcaleaaView Answer on Stackoverflow
Solution 7 - BashredolentView Answer on Stackoverflow
Solution 8 - BashragsView Answer on Stackoverflow
Solution 9 - BashJosephView Answer on Stackoverflow
Solution 10 - Bashuser3561136View Answer on Stackoverflow
Solution 11 - BashDaveView Answer on Stackoverflow
Solution 12 - BashZoltan K.View Answer on Stackoverflow
Solution 13 - BashDavidView Answer on Stackoverflow
Solution 14 - BashBill GaleView Answer on Stackoverflow
Solution 15 - Bash18446744073709551615View Answer on Stackoverflow
Solution 16 - Bashuser5207022View Answer on Stackoverflow
Solution 17 - Bashbuilder-7000View Answer on Stackoverflow
Solution 18 - BashDaniel BlackmonView Answer on Stackoverflow
Solution 19 - BashOndra ŽižkaView Answer on Stackoverflow