<!-- category: one-liners -->
Epoch to Human
You're staring at a log full of this: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:1709836800 user login accepted 1709836923 session timeout 1709837100 disk usage warning
Every epoch timestamp becomes a human date. Inline. No temp files. No manual anything.perl -pe 's~(\d{10})~localtime($1)~ge'
One substitution. The /e flag does all the work.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
Part 1: WHAT THE /e FLAG DOES
Normally, the replacement side ofs~~~ is a string. With /e, it's Perl code. Perl evaluates the expression and uses the return value as the replacement text.
WithoutPIECE 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
/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:
The epoch is seconds since January 1, 1970 UTC. Whensay 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
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:Or pass the file as an argument:cat /var/log/auth.log | perl -pe 's~(\d{10})~localtime($1)~ge'
Tail a live log with real-time conversion:perl -pe 's~(\d{10})~localtime($1)~ge' access.log
Thetail -f /var/log/app.log | perl -pe 's~(\d{10})~localtime($1)~ge'
-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:
Or do it all in Perl:grep ERROR app.log | perl -pe 's~(\d{10})~localtime($1)~ge'
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:Divide by 1000 before passing to1709836800000 event happened 1709836923456 another event
localtime:
Want to preserve the millisecond portion? Capture them separately:perl -pe 's~(\d{13})~localtime($1/1000)~ge'
Match 10 digits then 3 more. Convert the first 10, glue the remaining 3 back on with a dot:perl -pe 's~(\d{10})(\d{3})~localtime($1) . ".$2"~ge'
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.Thu Mar 7 12:00:00 2024.000 event happened Thu Mar 7 12:02:03 2024.456 another event
The lookaround assertionsperl -pe 's~(\d{10})(\d{3})~localtime($1).".$2"~ge; s~(?<!\d)(\d{10})(?!\d)~localtime($1)~ge'
(?<!\d) and (?!\d) in the second pass prevent matching digits that are part of something longer.
Part 5: CUSTOM DATE FORMATS WITH strftime
The defaultlocaltime output is fine for quick eyeballing. But sometimes you want ISO 8601 or something specific:
Output:perl -pe 'use POSIX qw(strftime); s~(\d{10})~strftime("%Y-%m-%d %H:%M:%S", localtime($1))~ge'
POSIX::strftime gives you full format control:2024-03-07 12:00:00 user login accepted 2024-03-07 12:02:03 session timeout
POSIX is a core module. No install needed. It ships with every Perl.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)
Part 6: UTC VS LOCAL TIME
localtime gives you your local timezone. For UTC, use gmtime:
Show both side by side:perl -pe 'use POSIX qw(strftime); s~(\d{10})~strftime("%F %T UTC", gmtime($1))~ge'
Output:perl -pe 's~(\d{10})~sprintf("%-24s (UTC: %s)", scalar localtime($1), scalar gmtime($1))~ge'
Handy when you're debugging across timezones and you need both at a glance.Thu Mar 7 07:00:00 2024 (UTC: Thu Mar 7 12:00:00 2024) user login
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:
This matches numbers starting with 1 followed by 9 more digits. Covers 2001 through 2286. Good enough for most logs.perl -pe 's~\b(1[0-9]{9})\b~localtime($1)~ge'
For a tighter range, use a conditional in the replacement:
Only converts if the value falls between year 2000 and 2033. Everything else passes through untouched. The ternary operator insideperl -pe 's~(\d{10})~($1 > 946684800 && $1 < 2000000000) ? localtime($1) : $1~ge'
/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):Output:perl -pe 's~(\d{10})~"$1 [" . localtime($1) . "]"~ge'
Best of both worlds. The epoch stays searchable, the human date is right there.1709836800 [Thu Mar 7 12:00:00 2024] user login accepted
Extract just the epochs from a file, one per line with conversion:
Theperl -nle 'print "$_: " . localtime($_) for m~(\d{10})~g'
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?"Time::Piece is core Perl. No install needed. Much friendlier than the oldperl -e 'use Time::Piece; print Time::Piece->strptime("2024-03-07 12:00:00", "%Y-%m-%d %H:%M:%S")->epoch, "\n"'
mktime with its 0-based months and 1900-based years.
Quick "epoch right now":
Quick "epoch for a given date" usingperl -e 'print time, "\n"'
POSIX::mktime:
That'sperl -e 'use POSIX qw(mktime); print mktime(0, 0, 12, 7, 2, 124), "\n"'
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:
Now pipe anything through it:alias epoch2date="perl -pe 's~(\d{10})~localtime(\$1)~ge'"
For the fancy version that handles both 10 and 13 digit timestamps:cat server.log | epoch2date grep ERROR app.log | epoch2date curl -s api.example.com/events | epoch2date kubectl logs mypod | epoch2date
The 13-digit pass runs first, then the 10-digit pass catches the rest. Order matters. Don't reverse them.alias epoch2date="perl -pe 's~(\d{10})(\d{3})~localtime(\$1).\".\$2\"~ge; s~(?<!\d)(\d{10})(?!\d)~localtime(\$1)~ge'"
Honorable mention: a histogram of events by hour.
Groups every timestamp by its hour and counts. Quick way to see when your system is busiest.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
perl.ggs~(\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..." // \ \ (| | ) /'\_ _/`\ \___)=(___/