perl.gg / one-liners

<!-- category: one-liners -->

Epoch to Human

2026-03-11

You're staring at a log full of this:
1709836800 user login accepted 1709836923 session timeout 1709837100 disk usage warning
Ten-digit numbers. Meaningless to human eyes. You could paste each one into some web converter like an animal. Or you could pipe it through five words of Perl:
perl -pe 's~(\d{10})~localtime($1)~ge'
Every epoch timestamp becomes a human date. Inline. No temp files. No manual anything.
Thu Mar 7 12:00:00 2024 user login accepted Thu Mar 7 12:02:03 2024 session timeout Thu Mar 7 12:05:00 2024 disk usage warning
One substitution. The /e flag does all the work.

Part 1: WHAT THE /e FLAG DOES

Normally, the replacement side of s~~~ is a string. With /e, it's Perl code. Perl evaluates the expression and uses the return value as the replacement text.
PIECE WHAT IT DOES -------------- ------------------------------------------ -p Read input line by line, print each line -e '...' Inline script s~(\d{10})~...~ge Substitution with flags (\d{10}) Capture exactly 10 consecutive digits localtime($1) Convert captured epoch to date string /g Global: replace ALL matches per line /e Evaluate replacement as Perl code
Without /e, the string localtime($1) would be inserted literally. With /e, Perl calls localtime(1709836800) and inserts the result. That's the whole trick.

Part 2: THE localtime FUNCTION

In scalar context, localtime returns a formatted date string in your local timezone:
say scalar localtime(0); # Wed Dec 31 19:00:00 1969 (in EST) # Thu Jan 1 00:00:00 1970 (in UTC) say scalar localtime(1_000_000_000); # Sat Sep 8 21:46:40 2001 say scalar localtime(time); # whatever right now is
The epoch is seconds since January 1, 1970 UTC. When localtime appears in the replacement side of s~~~e, it's already in scalar context. No need for the scalar keyword. It just returns the date string.

In list context it returns nine components (sec, min, hour, day, month, year, etc.), but that's not what we want here.

Part 3: PIPING FROM REAL TOOLS

This is where the one-liner earns its keep. Pipe anything through it:
cat /var/log/auth.log | perl -pe 's~(\d{10})~localtime($1)~ge'
Or pass the file as an argument:
perl -pe 's~(\d{10})~localtime($1)~ge' access.log
Tail a live log with real-time conversion:
tail -f /var/log/app.log | perl -pe 's~(\d{10})~localtime($1)~ge'
The -p flag handles line-by-line streaming naturally. Works with JSON, CSV, syslog, whatever. If there's a 10-digit number, it gets converted.

Combine with grep to filter first, then convert:

grep ERROR app.log | perl -pe 's~(\d{10})~localtime($1)~ge'
Or do it all in Perl:
perl -ne 'print if s~(\d{10})~localtime($1)~ge && /ERROR/' app.log

Part 4: MILLISECOND EPOCHS

JavaScript and Java love millisecond timestamps. Thirteen digits instead of ten:
1709836800000 event happened 1709836923456 another event
Divide by 1000 before passing to localtime:
perl -pe 's~(\d{13})~localtime($1/1000)~ge'
Want to preserve the millisecond portion? Capture them separately:
perl -pe 's~(\d{10})(\d{3})~localtime($1) . ".$2"~ge'
Match 10 digits then 3 more. Convert the first 10, glue the remaining 3 back on with a dot:
Thu Mar 7 12:00:00 2024.000 event happened Thu Mar 7 12:02:03 2024.456 another event
Order matters here. If you're processing a file that has both 10-digit and 13-digit timestamps, match the longer pattern first. Otherwise the 10-digit match eats the first 10 digits of your 13-digit timestamp and leaves three orphan digits hanging.
perl -pe 's~(\d{10})(\d{3})~localtime($1).".$2"~ge; s~(?<!\d)(\d{10})(?!\d)~localtime($1)~ge'
The lookaround assertions (?<!\d) and (?!\d) in the second pass prevent matching digits that are part of something longer.

Part 5: CUSTOM DATE FORMATS WITH strftime

The default localtime output is fine for quick eyeballing. But sometimes you want ISO 8601 or something specific:
perl -pe 'use POSIX qw(strftime); s~(\d{10})~strftime("%Y-%m-%d %H:%M:%S", localtime($1))~ge'
Output:
2024-03-07 12:00:00 user login accepted 2024-03-07 12:02:03 session timeout
POSIX::strftime gives you full format control:
FORMAT EXAMPLE OUTPUT -------- ---------------------- %Y-%m-%d 2024-03-07 %H:%M:%S 12:00:00 %F 2024-03-07 (shortcut for %Y-%m-%d) %T 12:00:00 (shortcut for %H:%M:%S) %F %T 2024-03-07 12:00:00 %a %b %d Thu Mar 07 %Z EST (timezone abbreviation) %s 1709836800 (epoch, back to where we started)
POSIX is a core module. No install needed. It ships with every Perl.

Part 6: UTC VS LOCAL TIME

localtime gives you your local timezone. For UTC, use gmtime:
perl -pe 'use POSIX qw(strftime); s~(\d{10})~strftime("%F %T UTC", gmtime($1))~ge'
Show both side by side:
perl -pe 's~(\d{10})~sprintf("%-24s (UTC: %s)", scalar localtime($1), scalar gmtime($1))~ge'
Output:
Thu Mar 7 07:00:00 2024 (UTC: Thu Mar 7 12:00:00 2024) user login
Handy when you're debugging across timezones and you need both at a glance.

Part 7: SELECTIVE CONVERSION

Not every 10-digit number is a timestamp. Phone numbers, IDs, zip codes with extensions. You might get false positives.

Only convert numbers that look like plausible epochs:

perl -pe 's~\b(1[0-9]{9})\b~localtime($1)~ge'
This matches numbers starting with 1 followed by 9 more digits. Covers 2001 through 2286. Good enough for most logs.

For a tighter range, use a conditional in the replacement:

perl -pe 's~(\d{10})~($1 > 946684800 && $1 < 2000000000) ? localtime($1) : $1~ge'
Only converts if the value falls between year 2000 and 2033. Everything else passes through untouched. The ternary operator inside /e is perfectly legal. It's just Perl code. Anything goes.

Part 8: KEEP THE ORIGINAL

Sometimes you want both the epoch (for grep and sort) and the human date (for your eyes):
perl -pe 's~(\d{10})~"$1 [" . localtime($1) . "]"~ge'
Output:
1709836800 [Thu Mar 7 12:00:00 2024] user login accepted
Best of both worlds. The epoch stays searchable, the human date is right there.

Extract just the epochs from a file, one per line with conversion:

perl -nle 'print "$_: " . localtime($_) for m~(\d{10})~g'
The m~~g in list context returns all matches. The for iterates. Each epoch gets its own line.

Part 9: THE REVERSE DIRECTION

Sometimes you need to go the other way. "What epoch is March 7, 2024 at noon?"
perl -e 'use Time::Piece; print Time::Piece->strptime("2024-03-07 12:00:00", "%Y-%m-%d %H:%M:%S")->epoch, "\n"'
Time::Piece is core Perl. No install needed. Much friendlier than the old mktime with its 0-based months and 1900-based years.

Quick "epoch right now":

perl -e 'print time, "\n"'
Quick "epoch for a given date" using POSIX::mktime:
perl -e 'use POSIX qw(mktime); print mktime(0, 0, 12, 7, 2, 124), "\n"'
That's mktime(sec, min, hour, mday, mon, year) where month is 0-based and year is years since 1900. March 7, 2024 at noon becomes mktime(0, 0, 12, 7, 2, 124). Yeah, the API is ugly. C heritage.

Part 10: MAKE IT A SHELL ALIAS

You'll use this enough to deserve an alias. Add to your .bashrc or .zshrc:
alias epoch2date="perl -pe 's~(\d{10})~localtime(\$1)~ge'"
Now pipe anything through it:
cat server.log | epoch2date grep ERROR app.log | epoch2date curl -s api.example.com/events | epoch2date kubectl logs mypod | epoch2date
For the fancy version that handles both 10 and 13 digit timestamps:
alias epoch2date="perl -pe 's~(\d{10})(\d{3})~localtime(\$1).\".\$2\"~ge; s~(?<!\d)(\d{10})(?!\d)~localtime(\$1)~ge'"
The 13-digit pass runs first, then the 10-digit pass catches the rest. Order matters. Don't reverse them.

Honorable mention: a histogram of events by hour.

perl -nle 'for (m~(\d{10})~g) { my @t = localtime($_); $h{sprintf "%02d:00", $t[2]}++ } END { print "$_: $h{$_}" for sort keys %h }' logfile.txt
Groups every timestamp by its hour and counts. Quick way to see when your system is busiest.
s~(\d{10})~localtime($1)~ge Input: "Event at 1709836800 ended" | (\d{10}) matches | localtime(1709836800) | "Thu Mar 7 12:00:00 2024" | Output: "Event at Thu Mar 7 12:00:00 2024 ended" .--. |o_o | "What time is 1709836800?" |:_/ | "Lemme check..." // \ \ (| | ) /'\_ _/`\ \___)=(___/
perl.gg