Accurate timestamping in Bash with printf (not a trivial problem)
As noted by @Isaac, with date
implementations that support %N
like GNU's or ast-open's, you can use %s%3N
to limit the precision, but except in ksh93
where date
can be made to be the builtin version of ast-open's date
, the date
command is not builtin. It will take a few hundred if not thousand microseconds to start and a few more to print the date and return.
bash
did copy a subset of ksh93
printf '%(...)T'
format, but not the %N
part.
Here it looks like you'd need to use more advanced shells like ksh93
or zsh
.
Those shells can make their $SECONDS
variable which records the time since the shell started (and that you can also reset to any value) floating point:
$ typeset -F SECONDS=0; date +%s%3N; echo $SECONDS
1506318780647
0.0017870000
It took up to 1787 microseconds to run GNU date
here.
You can use $((SECONDS*1000))
to get a number of milliseconds as both shells support floating point arithmetic (beware ksh93
honours the locale's decimal mark).
For the epoch time as a float, zsh
has $EPOCHREALTIME
:
$ zmodload zsh/datetime
$ echo $EPOCHREALTIME
1506318947.2758708000
And ksh93
can use "$(printf '%(%s.%N)T' now)"
(note that ksh93
's command substitution doesn't fork processes nor use pipes for builtins so is not as expensive as in other Bourne-like shells).
You could also define the $EPOCHREALTIME
variable there with:
$ EPOCHREALTIME.get() { .sh.value=$(printf "%(%s.%6N)T");
$ echo "$EPOCHREALTIME"
1506333341.962697
For automatic timestamping, you can also use set -o xtrace
and a $PS4
that prints the current time. In zsh
:
$ zsh -c 'PS4="+%D{%s.%.}> "; set -x; sleep 1; date +%s.%N'
+1506332128.753> sleep 1
+1506332129.754> date +%s.%N
1506332129.755322928
In ksh93:
$ ksh -c 'PS4="+\$(printf "%(%s.%3N)T")> "; set -x; sleep 1; date +%s.%N'
+1506332247.844> sleep 1
+1506332248.851> date +%s.%N
1506332248.853111699
Depending on your use case, you may be able to rely on moreutils
's ts
for your time-stamping:
$ (date +%s.%6N; date +%s.%6N) | ts %.s
1506319395.000080 1506319394.970619
1506319395.000141 1506319394.971972
(ts
gives the time it read the line from date
's output through the pipe).
Or for time between lines of output:
$ (date +%s.%6N; date +%s.%6N) | ts -i %.s
0.000011 1506319496.806554
0.000071 1506319496.807907
If you want to get the time it took to run a given command (pipeline), you can also use the time
keyword, adjusting the format with $TIMEFORMAT
in bash
:
$ TIMEFORMAT=%E; time date
Mon 25 Sep 09:51:41 BST 2017
0.002
Those time format directives initially come from csh (though bash
, contrary to zsh
or GNU time
only supports a tiny subset). In (t)csh, you can time every command by setting the $time
special variable:
$ csh -xc 'set time = (0 %E); sleep 1; sleep 2'
set time = ( 0 %E )
sleep 1
0:01.00
sleep 2
0:02.00
(the first number (0
here) tells that commands that take more than that many seconds should be timed, the second specifies the format).
Since bash5+ there is a simple solution:
$ printf "%.3f\n" $EPOCHREALTIME
1566917328.786
If the dot is not desired:
printf "%s\n" "$((${EPOCHREALTIME/.}/1000))"
Provide time in milliseconds since epoch all-in-one-line-of-text, atomic (one call to an internal bash variable), lightweight, and no quantization or rounding issues.
tl;dr
$ date +'%s%3N'
1506298414529
bash
An strict bash solution (no external executables) is possible since bash 4.2:
$ printf '%(%s)T\n' "-1"
1506298414
$ printf '%(%Y%m%d-%H:%M:%S)T\n' "-1"
20170924-20:13:34
But that doesn't allow milliseconds, nor nanoseconds (yet).
date
To get miliseconds or nanoseconds you need to use GNU date as this:
$ printf '%s\n' "$(date +'%Y%m%d-%H:%M:%S.%N')"
20170924-20:13:34.326113922
Or
$ printf '%s\n' "$(date +'%s.%N')"
1506298414.529678016
The limit to 3 digits in the fractional part of the seconds could be produced with a %.3f
format for printf:
$ printf '%.3f\n' "$(date +'%s.%N')"
1506298414.529
Or better, use the ability to reduce the number of digits that the date nanoseconds format allow:
$ printf '%s\n' "$(date +'%s.%3N')"
1506298414.529
And then, the dot could be removed:
$ printf '%s\n' "$(date +'%s%3N')"
1506298414529
Of course, in this case, the simpler solution (without printf, instead of what was exactly asked) seems more apropiate:
$ date +'%s%3N'
1506298414529