I have a file that contains epoch dates which I need converting to human-readable. I already know how to do the date conversion, eg:

[server01 ~]$ date -d@1472200700
Fri 26 Aug 09:38:20 BST 2016

..but I'm struggling to figure out how to get sed to walk through the file and convert all the entries. The file format looks like this:

ll /data/holding/email
cat /etc/rsyslog.conf
ll /data/holding/web
  • 1
    For future reference (assuming this is a Bash history file; it looks like one), look to the HISTTIMEFORMAT shell variable to control the format at time of writing. Aug 30, 2016 at 14:44
  • @Toby the value of HISTTIMEFORMAT is used when displaying (to stdout), but only its status (set to anything even null vs unset) matters when writing HISTFILE. Sep 3, 2016 at 10:43
  • Thanks @dave, I didn't know that (not being a user of history times myself). Sep 5, 2016 at 9:00
  • date -d is not portable to say Solaris... I'm assuming this is on a system with mostly GNU tools? (GNU AWK / Perl tend to be the more portable methods to deal with date conversions). gawk '{ if ($0 ~ /^#[0-9]*$/) {print strftime("%c",substr($0,2)); } else {print} }' < file (strftime seems non-portable...) Jan 13, 2017 at 8:07

6 Answers 6


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'


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

  • 1
    What about using just a single instance of date -f to do all the conversions in a stream-wise manner? Aug 30, 2016 at 20:58
  • The perl command seems to add a new line after ctime $1 and I can't find any way to remove it. Jun 23, 2017 at 7:47
  • 2
    @Alex. Right. See edit. Adding the s flag makes so that .* also includes the newline on input. You can also use strftime "%c", localtime $1. Jun 23, 2017 at 14:02
  • @StéphaneChazelas thanks so much. It's a great answer. Jun 23, 2017 at 14:04
  • Where does the perl command reference the file it is using as input and output?
    – zylstra
    Sep 5, 2021 at 7:46

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


$ cat file.txt
ll /data/holding/email
cat /etc/rsyslog.conf
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
  • Nice....that prints the converted date to screen, now how do I get that command to replace the entries in the file?
    – machinist
    Aug 30, 2016 at 8:01
  • @machinist Check my edits..
    – heemayl
    Aug 30, 2016 at 8:17
  • 1
    If you are using a recent version of bash, printf can do the conversion itself: printf '#%(%F %H)T\n' "${BASH_REMATCH[1]}".
    – chepner
    Aug 30, 2016 at 19:54

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 to date -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' tells paste 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.

  • date -d (from the question) is also non-portable... (On FreeBSD it will try to mess with DST settings, on Solaris it will give an error...) The question does not specify an OS though... Jan 13, 2017 at 8:14
  • @GertvandenBerg yes, this is addressed in the last paragraph of this answer. Jan 13, 2017 at 15:56
  • I mean that the asker's sample also has portability issues... (They should probably have tagged an OS...) Jan 14, 2017 at 16:06

Assuming the date format you have in your post is what you want, the following regex should fit your needs.

sed -E 's/\#(1[0-9]{9})(.*)/echo \1 $(date -d @\1)/e' log.file

Be mindful of the fact this will only replace one epoch per line.

  • I'm getting the following error with that command: sed: -e expression #1, char 48: invalid reference \3 on 's' command's RHS
    – machinist
    Aug 30, 2016 at 7:53
  • 1
    My mistake, edited the post.
    – Hatclock
    Aug 30, 2016 at 8:25

using sed :

sed -r 's/\#([0-9]*)/echo $(date -d @\1)/eg' test.txt

output :

ر أغس 24 16:09:55 EET 2016
ll /data/holding/email
ر أغس 24 16:11:46 EET 2016
cat /etc/rsyslog.conf
ر أغس 24 16:13:58 EET 2016
ll /data/holding/web

as my locale language is Arabic :)


My solution how to do that in a pipeline

cat test.txt | sed 's/^/echo "/; s/\([0-9]\{10\}\)/`date -d @\1`/; s/$/"/' | bash

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .