18

I just noticed, that POSIX date doesn't seem to have the %s or %N format items. So I can't use those. What's an alternative, yet POSIX-compliant way to get the epoch timestamp in my shell script?

4
  • I'm not sure what a Posix-compatible stat command is supposed to print, but you might be able to touch a temporary file, and get its mtime as seconds-since-the-epoch. May 19 at 23:11
  • On a GNU system you can use touch tmpfile; stat --format=%Y tmpfile. On a BSD system you can use touch tmpfile; stat -f %Um tmpfile. (I grant that neither of these is POSIX.) May 20 at 0:08
  • 1
    @SteveSummit, it isn't supposed to print anything, since there's no such command listed in the specification... (pubs.opengroup.org/onlinepubs/9699919799.2018edition/idx/…). On a GNU or BSD system they could just use date +%s. Probably in a lot of others too.
    – ilkkachu
    May 20 at 8:37
  • @ilkkachu Yes, well, there's a reason I'm entering these ideas as comments, not answers. :-) Writing a small C program, as in the first half of your answer, is IMO clearly the right way to go. (Me, I don't even have to; I've got five or six tools in my personal toolbox that will do it. :-) ) May 20 at 11:09

3 Answers 3

26

For the epoch time as an integer number of seconds, that would be:

awk 'BEGIN{srand(); print srand()}'

or:

awk 'BEGIN{print srand(srand())}'

As in POSIX awk, srand() without argument uses the current time to seed the pseudo-random generator. It also returns the previous seed, so the second srand() above returns the epoch time that was used for the previous one¹.

You can get the fractional part with something like:

echo|LC_ALL=C TZ=UTC0 diff -u /dev/null - |sed -n '2s/.*\(\.[0-9]*\).*/\1/p'

POSIX does specify the output format for diff -u, and that the current time be used when the file is -.

But several thousands if not millions of nanoseconds will likely have elapsed since you called awk earlier before you get the output, it may not even be the same second. You may however be able to check if it's the case by comparing <epochtime> % 60 with the second part of the (UTC) timestamp in the diff header if you were so inclined.


¹ About that awk solution, note that POSIX used not to say it in so many words. I did raise an objection to POSIX some time ago about the unclear wording, also stating that it was unreasonable in this day and age to force implementations to use that poor a source of entropy. Instead the resolution was to explicitly require srand() use the epoch time.

7
  • 18
    well, I didn't expect this :D
    – ilkkachu
    May 19 at 13:22
  • 4
    @ilkkachu see also austingroupbugs.net/view.php?id=983#c3281 for an even more convoluted alternative. May 19 at 13:38
  • 5
    @StéphaneChazelas On the one hand, this is certainly an... imaginative solution. But on the other hand, and I hope you don't mind my saying this, it is absolutely hideous! Me, I'd never use it; it's just way too obvious that (despite tortured discussions such as you referenced) it might not work everywhere, or forever. May 19 at 23:21
  • 1
    I love this answer, and it's really clever, but I don't think it's correct. POSIX specifies for awk's srand: "Set the seed value for rand to expr or use the time of day if expr is omitted. The previous seed value shall be returned." But it does not say what form of the "time of day" is "used", and seems to allow for the implementation to chose something like a hash of the epoch timestamp, just the HH:MM:SS part, a timestamp against a different epoch, etc. May 20 at 14:46
  • 1
    @R..GitHubSTOPHELPINGICE see the footnote. May 20 at 15:03
13

Write a C program that calls time() and prints the result. Borrowing from sample program in the specification of the time() function, let's call this e.g. seconds.c:

#include <stdio.h>
#include <stdint.h>
#include <time.h>

int main(void)
{
    time_t t = time(NULL);
    printf("%ju\n", (uintmax_t) t);
    return 0;
}

and compile with c99 -o seconds seconds.c. (If I read the spec correctly, that should be a standard way to compile it.)


Alternatively, you could take the decomposed date and calculate the number of seconds from that. It's relatively easy since all days are 86400 seconds long. We do need to get rid of the leading zeroes from some values so that they're not taken as octal by the shell arithmetic:

As a shell function:

epochtime() {
    eval "$(date -u +'y=%Y j=%j h=%H m=%M s=%S')"
    s=${s#0}
    m=${m#0}
    h=${h#0}
    j=${j#0}
    j=${j#0}
    t=$(( ((y-1970)*365 + (y-1969)/4 + j - 1)*86400 + h*3600 + m*60 + s))
    echo "$t"
}

Note on the leap year calculation: The first leap year after the epoch is 1972, and (1972-1969)/4 = 3/4 = 0 , while (1973-1969)/4 = 4/4 = 1 (relying on integer truncation). Hence (year-1969)/4 gives the number of leap years before the current year. The possible leap day in the current year is included in the %j value, the "day of year". That one counts from 1 up, but we want the number of complete days elapsed, hence the minus one in the expression.

8
  • I think the shell function will be inconsistent if the current year is a leap year and you're calling it before 2/29. You add in the extra day in the year before the day actually occurs.
    – doneal24
    May 19 at 15:44
  • @doneal24, it should be ok, actually. (y-1969)/4 only gives 1 in 1973, the year after the first leap year after the epoch. Any leap day in the current year comes from %j. You can change the first line to e.g. eval "$(date -d "${1-now}" -u +'y=%Y j=%j h=%H m=%M s=%S')" and then test e.g. epochtime '2020-01-01 12:00' against date -u -d '2020-01-01 12:00' +%s. (With a date that has -d)
    – ilkkachu
    May 19 at 16:12
  • 1
    Do you know what will be the output of your function on 01-01-2107 ? Is it correct ?
    – user232326
    May 19 at 18:57
  • 1
    @IsaaC I'll be dead by then and then it's not my problem anymore.
    – MechMK1
    May 19 at 20:49
  • 1
    @MechMK1 You do not need to be in 2107 to try that date. Try faketime 2107-01-01 ./script.
    – user232326
    May 21 at 15:56
6

You can do it with a custom date format string that produces a shell arithmetic expression for the POSIX formula for seconds since the epoch:

secs=$((`TZ=GMT0 date \
+"((%Y-1600)*365+(%Y-1600)/4-(%Y-1600)/100+(%Y-1600)/400+1%j-1000-135140)\
*86400+(1%H-100)*3600+(1%M-100)*60+(1%S-100)"`))

This approach is taken from https://www.etalabs.net/sh_tricks.html

2
  • ok, that's a nice trick for dealing with the leading zeroes.
    – ilkkachu
    May 20 at 13:55
  • @ilkkachu: The original version had a bug there that was fixed by a reader's bug report and suggestion to use the 1 prefix. May 20 at 14:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.