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