Episode: 4678
Title: High Resolution Elapsed Time in Shell Scripts
Source: https://hub.hackerpublicradio.org/ccdn.php?filename=/eps/hpr4678/hpr4678.mp3
Transcribed: 2026-07-31 16:16:39 (official HPR transcript)

---

This is Hacker Public Radio Episode 4678, for 2026-07-08
Today's show is entitled, "High Resolution Elapsed Time in Shell Scripts"
The host is Whiskeyjack and the duration is 00:28:49
The flag is Clean, and the license is CC-BY-SA
The summary is "Surprises encountered when measuring elapsed time in shell scripts"
In this episode, I will describe how to calculate elapsed time in bash or other shell scripts.
Well, this may sound like a very simple and basic thing to do.
There's a slightly more complex aspect to it if you wish to calculate elapsed time
to a higher resolution than one second.
There are many reasons for calculating elapsed time in a shell script.
For example, you may wish to simply report how long an operation took to run.
Another reason may be that you're trying to speed up a script
and need to calculate benchmark data to see how different alternatives methods perform.
What may seem like a simple task gets a bit more complicated
if you want to do it for multiple different operating systems,
even if they are all unix-related as we shall see.
Operating systems tested.
For the purposes of this episode, I ran tests on the current version of the following operating systems.
Elma, Alpine, Debian, FreeBSD, OpenBSD, Resbury Pi, OpenSouth,
Ubuntu 2604.
Elma is a close copy of Red Hat that we can take as representing Red Hat style distros.
Simple, low-resolution timing.
I will start with the simple and obvious method before describing the less obvious ones.
This uses the date command to get the current time in seconds since the Unix epoch.
This is simply date that's D-A-T-E, space, single-quote, plus percent, lower-case S, single-quote.
Save this to a variable using whatever method you prefer.
For example, start time equals dollar sign, left bracket, date, space, single-quote, plus percent S, single-quote, right bracket.
Next, do whatever operations it is you wish to time.
Use the date command to get the current time again.
End time equals dollar sign, left bracket, date, space,
Quote, plus percent S, quote, right bracket.
Now, simply subtract the start time from the end time using shell arithmetic.
This should be very obvious and basic.
Higher resolution timing.
However, suppose we wish to measure time to greater than one second of precision.
We need to do two things.
The first is to obtain the current time at a higher degree of precision.
The second is to conduct the calculations to a higher degree of precision.
Unfortunately, the standard time precision for posit shells seems to be one second.
Some shells offer a higher precision, but others do not.
Furthermore, standard shell arithmetic uses integer, which limits calculations to one second of precision.
Bash, high-resolution shell variable.
Fortunately, Bash is one that does offer a high precision date.
If you are using Bash 5.0 or newer, there is a shell variable called e-pock-real-time
that is capitals, E-P-O-C-H-R-E-A-L-T-I-M-E,
which offers time since the Unix-E-Pock that is since the first January 1970 at zero zero zero UTC
in seconds to six decimal of precision.
Example, echo, dollar sign E-Pock-real-time
will give the result of one seven seven nine six three four eight zero zero,
decimal one eight four nine two six.
This is related to the similar Bash variable known as e-pock-seconds that is capitals
E-P-O-C-H-S-E-C-O-N-D-S, which gives the number of seconds since the Unix-E-Pock.
Example, echo, dollar E-Pock-Seconds,
to should print out an integer number somewhere along the lines of
one seven seven nine six three four eight zero zero.
So if you're using Bash, measuring time is very simple.
Easier script, however, actually using Bash.
Debian and derivatives actually have two shells.
The first, the interactive shell is Bash.
The second, the non-interactive shell is Dash, which stands for Debian,
Almquist Shell.
If you open a terminal, you get Bash.
If your script starts with a bin Bash Shabangline, you get Bash.
However, if your script starts with a bin S-H Shabangline,
you get Dash that's D-A-S-H.
Some people find themselves getting caught out by this one.
When they try something out in a terminal,
but find that it doesn't work in their script,
which started with a bin S-H.
Many other, but not all,
Linux-Tistros use Bash for both the interactive
and non-interactive shells, so bin S-H
and bin Bash work the same with those ones.
If you intend to use Bash,
make sure your script calls for Bash in the first line.
The shell variable.
So how can a script tell what shell it is running under?
There's a shell variable called shell that's capital S-H-E-L-L,
which will tell you the name of the shell.
Well, sort of.
On Debian and derivatives, shell will say Bash,
regardless of whether the actual shell is Bash or Dash.
On some other operating systems, shell will simply say S-H,
even if it is something else entirely.
So we need to do some additional levels of checking
to see what we have.
To start with though, here's what each of the testistros reports for shell.
Alma says Bash, alpine says S-H,
Debian says Bash, free BSD says S-H,
open BSD says K-S-H,
Raspberry Pi says Bash, open Sous says Bash,
Ubuntu says Bash.
Bash versus Dash.
First, let's try to see which ones are Bash
and which ones are Dash.
The first thing we can check for is the shell variable Bash version
that is capital B-A-S-H underscore
V-E-R-S-I-O-N.
Example, echo, dollar sign, Bash version.
If the shell is Bash, that it will report a version string.
If the shell is not Bash, then it will return an empty value.
Using this test, we can see that alma
and open Sous are indeed using Bash.
We however need to check Debian, Raspberry Pi, and Ubuntu,
when running an S-H script.
To check this, we can use the which command
this W-H-I-C-H
to see what S-H actually is.
Example, echo, space, dollar sign,
left bracket, L-S, space, dash L, space,
dollar sign, left bracket, which space, S-H.
Right bracket, space, pipe symbol, space, R-E-V, space,
pipe symbol, space, cut that C-U-T, space, dash D,
double quote, space, double quote, space,
dash F-1, space, pipe symbol, space, cut, space,
dash D, slash, space, dash F-1, space, pipe symbol, space,
R-E-V, space, right bracket, which space, S-H
shows us the path to S-H.
However, this is a link, so we need to use
L-S, space, dash L to find the actual executable.
R-E-V reverses the string, cut, takes the first element,
separated by spaces.
The second cut takes the first element, separated by
the slash characters.
The final R-E-V takes that string and reverses it again
to get it in the correct order.
In the case of Debian, Raspberry Pi, and Ubuntu,
it tells us that this is dash.
Open B-S-D. Open B-S-D reports its shell as K-S-H,
which stands for cornshell.
It is indeed cornshell, so we can simply leave that one as is.
Alpine and free B-S-D.
Next, we have Alpine Linux and free B-S-D,
which both report as S-H.
In the case of free B-S-D, there doesn't appear to be any further we can go that I'm aware of.
It's simply S-H.
It is a basic posic shell, which seems to be similar to the original Unix shell,
the born shell.
Older versions of free B-S-D use a different shell,
known as T-S-C-H, or the C shell.
But I haven't tested that, so I will ignore that here.
With Alpine Linux, however, we can get the actual shell
using the same method that we use for Debian Linux.
This report is being busy box.
Busy box is a limited shell, intended for use in embedded systems.
Alpine was originally an embedded digital,
but some people started using it for containers.
Alpine is Linux, but it is not GNU Linux,
and there are a number of areas which can trip you up if you're not aware of them.
So be extra careful if you're using it for anything
and test everything.
Summary of actual shells.
Here is our revised list with the actual shell used,
when asking for S-H, so far as we can determine.
Alma uses bash, Alpine uses busy box,
Debian uses dash, free B-S-D uses S-H,
open B-S-D uses K-S-H,
Raspberry Pi uses dash,
Susa uses bash,
and Ubuntu uses dash.
There are other shells,
but none of them are the default shell
for any of the distros on our list,
so I haven't tested them.
Solutions for measuring time.
Now we need to find solutions for bash,
dash, K-S-H, S-H, and busy box.
Bash.
For bash, we can simply use apoc real-time,
as mentioned above.
Dash.
For dash, we can use the date command.
This is a very conventional method,
and it's probably the first answer
that anyone would give for this situation.
However, while it will work in most cases,
it will not work in all cases,
so it is not a universal solution.
To use date, we simply call it with the correct format string.
This uses percent lower-case S to get
seconds to see apoc,
and percent upper-case N,
to get nanoseconds of the current second.
If you put a decimal separator between the two,
it will appear in the output.
You can use the correct decimal separator for your local,
but it won't go into that here.
Instead, I will just assume a period or dot.
Example, date, space, single-quote,
plus percent lower-case S, dot,
percent upper-case N, single-quote.
This will output a number,
which is the number of seconds as apoc,
and then a dot, and then the number of nanoseconds.
Problems with date on alpine and open BSD.
Date will work for bash, dash,
and SH on free BSD.
However, it will not work for KSH on open BSD,
or for busybox on alpine.
With busybox on alpine,
it simply ignores the percent and format specifier,
and prints out the apoc in seconds only,
followed by the decimal separator.
With KSH on open BSD,
it prints the apoc in seconds,
followed by the decimal separator,
and then the percent N as a literal N.
Fortunately, we have alternatives for these two cases.
Open BSD.
Open BSD has the TS or timestamp utility,
installed by default.
TS prints a timestamp in front of every line,
it receives from standard input.
I won't go into details on all aspects of TS here.
I'll leave that to someone else.
Instead, I will focus on how to use it for our specific purposes here.
We need to provide a format specifier to TS,
which in this case is,
quote,
percent dot,
lowercase as quote.
We also need to provide something for standard input,
otherwise TS will simply sit there
and wait for input.
So what we need to do is to echo nothing through a piped TS,
or also giving TS the proper format specifier.
Example, echo, space, pipe symbol, space, TS, space,
double quote, percent dot, lowercase as double quote.
This will provide the apoc time in seconds to six decimals of precision.
TS is installed in OpenBSD and FreeBSD by default,
and can be used in either.
It can also be installed in many other distros.
Busybox on Alpine.
None of the methods discussed so far
will work for busybox on Alpine though.
However, there is a way,
but it's a bit non-obvious and somewhat hacky.
Busybox includes a command called
A-D-J-T-I-M-E-X,
which I will pronounce adjust time X.
This is normally used to adjust the time hardware.
However, if it is run without arguments,
it will report the current settings.
These include the current apoc time in seconds,
and in another field, the time in microseconds.
These are reported as key value pairs.
So what we need to do is the following.
Run adjust time X as A-D-J-T-I-M-E-X,
capture the output,
grip for time dot TV underscore,
SEC, grip for time dot TV,
underscore U-S-E-C,
use cut to extract the time value in each case,
use TR to get rid of excess spaces in each case,
combine the two in a string
with a decimal separator between them.
This takes a total of four lines of shell script.
I will just describe them briefly here.
See the show notes for details.
First, we want to capture the output
of adjust time X in a single operation.
Run adjust time X
and pipe the output through grip to capture lines containing
TI-M-E dot TV underscore
and save this to a variable.
Next, echo the contents of this variable
and pipe it through grip, cut, and TR
to get first the seconds and then the microseconds
will also removing excess spaces,
save these to two separate variables.
Adjust time X does not zero pass the microsecond time value
to provide dating zeros.
So we need to take care of this using printF
before we can append it to the second value.
We didn't need to do this with date
where the percent n format character does this automatically.
In this instance, the printF format string is single-quote
percent zero-six D single-quote.
Now, combine these into a single number
with a decimal separator
by using simple string concatenation.
Summary of methods.
Let's summarize where we are so far in terms of methods
that we can use to get the current time
as a high-resolution number for Alma
or Debian with Bash or Raspberry Pi with Bash
or Ubuntu with Bash
or Susa use apocryl time or date.
For Debian with Bash or Raspberry Pi with Bash
or Ubuntu with Bash use date.
For Alpine, use Adjust Time X
and Parsee output.
For FreeBSD use date or TS
for OpenBSD use TS.
Other alternatives,
through a few alternatives that we haven't discussed yet.
Bash with Bash.
In the case of Debian, Raspberry Pi and Ubuntu running dash,
since Bash is available,
it is possible to write a separate Bash script
which simply echoes apocryl time
and then call it from the dash script
and capture the output.
Well, this would work,
there's probably not a lot of point to it.
If you can rely on Bash being there,
then just change the first line to the script
and make it a Bash script.
Adding Packages to Alpine.
The TS or Time Stamp Utility
is a common unix utility
that can be installed if it is not present by default.
This does produce high-resolution timestamps on Alpine.
On Alpine Linux, this comes as part of the more utils package
to add the package used the following.
Suedu, APK, add more utils.
You can also add the GNU Core Utils,
which will provide a high-resolution date command,
which works like in the other examples.
To add the package, use the following.
Suedu, APK, add Core Utils.
If you can install more packages into your Alpine system,
either of the above two is probably going to be preferable
to parsing the output of adjust time X.
Custom Time Stamp Programs.
You could also write a very short program in Python,
Pearl, tickle, or some other language,
and have it output the current APK time.
I won't discuss that here though.
Calculating time differences.
Scholar arithmetic isn't a juror only.
If we wish to use high-resolution timing data,
we need to do something so that we don't lose a precision
we have work so hard to get.
There are several possible solutions.
Change the time base.
One method is to change the time base from seconds to
Billy, Micro, or Nano seconds.
This can be done by simply multiplying the time values
by the appropriate amount, say a thousand, or million, etc.
Before subtracting them.
This allows for integer arithmetic on high-resolution values
without losing precision.
Use the shell, BC, Herbatory Precision Calculator.
The BC command line calculator will perform calculations using real numbers
and is easy to use in scripts.
It is present by default in most districts.
For example, echo, space, double-quote, scale equals nine,
semicolon, space, dollar sign, end time, space, minus space,
dollar sign, start time, double-quote, space, pipe symbol, space, BC.
Where end time and start time are variables containing time values.
However, for some inexplicable reason, neither debion
nor open sues installed by default.
It is present in Ubuntu and Raspberry Pi which are debion derivatives
and it can be added to districts which lack it.
Use, oc. oc can also perform calculations using real numbers
and it is present in dearly all districts including
and all of the ones we tested here.
Here's an example.
echo, space, double-quote, dollar sign, end time, space,
dollar sign, start time, double-quote, space, pipe symbol, space, oc.
That's a WK, space, single-quote, curly bracket,
print-f, space, double-quote, percent, dot, six, f,
back slash, end, double-quote, comma, space, dollar sign,
one, space, minus space, dollar sign, two, right curly bracket, single-quote.
benchmarks.
And of course, no comparative evaluation would be complete
with a benchmarks where we can see how each method compares to another in terms of speed.
In the benchmark test, I ran each method in the loop through multiple iterations,
measured the laps time, subtracted out the time from an empty loop,
and then compared it to the alternate methods.
For anything other than apoc real time, the empty loop time is negligible
and has no really fact on the results.
Rather interestingly, I came across a bug which caused date to run very slowly
called immediately after using apoc real time in bash.
The effect of the bug was to make the date benchmark test run roughly 24 times slower.
This has been fixed in newer releases, but if you're using an older gesture release,
then be aware of this bug.
I was able to get around it by either putting a sleep delay between benchmarking apoc real time
and benchmarking date, or by simply testing date before testing apoc real time.
To be able to conduct additional tests, I installed TS in Ubuntu and Alpine
and the GNU version of date in Alpine.
Apoc real time versus date in Ubuntu 2604 bash.
The apoc real time method is 3100 and 3 times faster than just date.
However, when the same test is run on Ubuntu 2404, when the date test is run before
the apoc real time test, apoc real time is 1,240 times faster than date.
Other Linux test rows show performance similar to Ubuntu 2404.
It appears that a side effect of fixing whatever the bug is as the effect of slowing down date.
However, this is probably not a significant issue in normal circumstances.
Date versus TS in Ubuntu 2604 bash.
The date method is 3.7 times faster than TS.
Date versus TS in Ubuntu 2604 dash.
The date method is 4.9 times faster than TS.
Date versus TS in free BSD as H.
The date method is 2.5 times faster than TS.
Date versus adjust time X and Alpine busy box.
The date method is 6 times faster than adjust time X.
Date versus TS and Alpine busy box.
The date method is 20 times faster than TS.
B.C. versus Ock. It Ubuntu 2604.
I compare calculating the difference between the two numbers when using B.C. versus Ock.
The difference is negligible with B.C. being only 7% faster than Ock.
Conclusion for benchmarks. Based on these results, if you need to measure elapsed time to high resolution
and care about running the command with as little overhead as possible,
then the order preference should be the following.
If you're using a newer version of bash, then use a Pock real time.
If that is not available, then use date provided it allows for high resolution times.
If the above two cannot be used, then use TS.
If you're using busy box and cannot install either GNU data or TS,
then use adjust time X.
Date is a closest in terms of being the universal portable solution,
but it does not work in all cases.
I have not compared different platforms to each other in terms of performance,
as that would be a much more involved problem that is outside the scope of this episode.
However, different operating systems implement commands in different ways.
For example, on OpenBSD and FreeBSD, TS appears to be an elf binary.
That is, it is executable machine code, possibly written in C.
On Ubuntu, however, TS appears to be a Pearl script.
As a result of this, the advantage that date has over TS is much less in FreeBSD
than it is with Ubuntu and likely other Linux tutorials.
As on FreeBSD, it doesn't need to load a Pearl interpreter to run TS.
Overall, conclusion.
You know, don't thought that measuring elapsed time was going to be so simple
and how could someone get an entire podcast out of such a simple subject.
And yet here we are half an hour later, with just a basic overview of the subject.
I hope you found this interesting and informative.
Please let us know in the comments if you think that I have done anything incorrectly,
or if you have another way of doing things.
I hope to see you all again in another future episode of HPR.
You have been listening to the Hacker Public Radio podcast, at hackerpublicradio.org.
Today's show was contributed by a HPR listener like yourself.
If you ever thought of recording a podcast, then visit the HPR site to find out how easy it really is.
Hosting for HPR has been kindly provided by anhonesthost.com, the Internet Archive, rsync.net, and the HPR Community Content Delivery Network.
Unless otherwise stated, today's show is released under a Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
