How to calculate time elapsed in bash script?

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.


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

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.