How to replace epoch timestamps in a file with other formats?
While it's possible with GNU sed
with things like:
sed -E 's/^#([0-9]+).*$/date -d @\1/e'
That would be terribly inefficient (and is easy to introduce arbitrary command injection vulnerabilities1) as that would mean running one shell and one date
command for each #xxxx
line, virtually as bad as a shell while read
loop. Here, it would be better to use things like perl
or gawk
, that is text processing utilities that have date conversion capabilities built-in:
perl -MPOSIX -pe 's/^#(\d+).*/ctime $1/se'
Or:
gawk '/^#/{$0 = strftime("%c", substr($0, 2))};1'
1 If we had written ^#([0-9]).*
instead of ^#([0-9]).*$
(as I did in an earlier version of this answer), then in multi-byte locales like UTF-8 ones (the norm nowadays), with an input like #1472047795<0x80>;reboot
, where that <0x80>
is the byte value 0x80 which does not form a valid character, that s
command would have ended up running date -d@1472047795<0x80>; reboot
for instance. While with the extra $
, those lines would not be substituted. An alternative approach would be: s/^#([0-9])/date -d @\1 #/e
, that is leave the part after the #xxx
date as a shell comment
Assuming consistent file format, with bash
you can read the file line by line, test if it's in given format and then do the conversion:
while IFS= read -r i; do [[ $i =~ ^#([0-9]{10})$ ]] && \
date -d@"${BASH_REMATCH[1]}"; done <file.txt
BASH_REMATCH
is an array whose first element is the first captured group in Regex matching, =~
, in this case the epoch.
If you want to keep the file structure:
while IFS= read -r i; do if [[ $i =~ ^#([0-9]{10})$ ]]; then printf '#%s\n' \
"$(date -d@"${BASH_REMATCH[1]}")"; else printf '%s\n' "$i"; fi; done <file.txt
this will output the modified contents to STDOUT, to save it in a file e.g. out.txt
:
while ...; do ...; done >out.txt
Now if you wish, you can replace the original file:
mv out.txt file.txt
Example:
$ cat file.txt
#1472047795
ll /data/holding/email
#1472047906
cat /etc/rsyslog.conf
#1472048038
ll /data/holding/web
$ while IFS= read -r i; do [[ $i =~ ^#([0-9]{10})$ ]] && date -d@"${BASH_REMATCH[1]}"; done <file.txt
Wed Aug 24 20:09:55 BDT 2016
Wed Aug 24 20:11:46 BDT 2016
Wed Aug 24 20:13:58 BDT 2016
$ while IFS= read -r i; do if [[ $i =~ ^#([0-9]{10})$ ]]; then printf '#%s\n' "$(date -d@"${BASH_REMATCH[1]}")"; else printf '%s\n' "$i"; fi; done <file.txt
#Wed Aug 24 20:09:55 BDT 2016
ll /data/holding/email
#Wed Aug 24 20:11:46 BDT 2016
cat /etc/rsyslog.conf
#Wed Aug 24 20:13:58 BDT 2016
ll /data/holding/web
All the other answers spawn a new date
process for every epoch date that needs to be converted. This could potentially add performance overhead if your input is large.
However GNU date has a handy -f
option that allows a single process instance of date
to continuously read input dates without the need for a new fork. So we can use sed
, paste
and date
in this manner such that each one only gets spawned once (2x for sed
) regardless of how large the input is:
$ paste -d '\n' <( sed '2~2d;y/#/@/' epoch.txt | date -f - ) <( sed '1~2d' epoch.txt )
Wed Aug 24 07:09:55 PDT 2016
ll /data/holding/email
Wed Aug 24 07:11:46 PDT 2016
cat /etc/rsyslog.conf
Wed Aug 24 07:13:58 PDT 2016
ll /data/holding/web
$
- The two
sed
commands respectively basically delete even and odd lines of the input; the first one also replaces#
with@
to give the correct epoch timestamp format. - The first
sed
output is then piped todate -f
which does the required date conversion, for every line of input that it receives. - These two streams are then interlaced into the single required output using
paste
. The<( )
constructs are bash process substitutions that effectively trick paste into thinking it is reading from given filenames when it is in fact reading the output piped from the command inside.-d '\n'
tellspaste
to separate odd and even output lines with a newline. You could change (or remove) this if for example you want the timestamp on the same line as the other text.
Note that there are several GNUisms and Bashisms in this command. This is not Posix-compliant and should not be expected to be portable outside of the GNU/Linux world. For example date -f
does something else on OSXes BSD date
variant.