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:
-
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 ))
-
-
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 forstrftime(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.