Как заменить метки времени в файле другими форматами?

10

У меня есть файл, который содержит даты эпох, которые мне нужно преобразовать в удобочитаемые человеком. Я уже знаю, как сделать преобразование даты, например:

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

... но я изо всех сил пытаюсь выяснить, как sedпройтись по файлу и преобразовать все записи. Формат файла выглядит следующим образом:

#1472047795
ll /data/holding/email
#1472047906
cat /etc/rsyslog.conf
#1472048038
ll /data/holding/web
машинист
источник
1
Для дальнейшего использования (при условии, что это файл истории Bash; он похож на файл), посмотрите HISTTIMEFORMATпеременную оболочки, чтобы контролировать формат во время записи.
Тоби Спейт
@Toby значение HISTTIMEFORMAT используется при отображении (в stdout), но при записи HISTFILE имеет значение только его состояние (задано равным нулю или не установлено).
dave_thompson_085
Спасибо @dave, я этого не знал (я сам не был пользователем истории).
Тоби Спейт
date -dне является переносимым, чтобы сказать, Solaris ... Я предполагаю, что это в системе, в основном с инструментами GNU? (GNU AWK / Perl, как правило, являются более переносимыми методами для преобразования дат). gawk '{ if ($0 ~ /^#[0-9]*$/) {print strftime("%c",substr($0,2)); } else {print} }' < file( strftimeкажется
Герт ван ден Берг

Ответы:

6

Предполагая согласованный формат файла, bashвы можете прочитать файл построчно, проверить, находится ли он в заданном формате, а затем выполнить преобразование:

while IFS= read -r i; do [[ $i =~ ^#([0-9]{10})$ ]] && \
      date -d@"${BASH_REMATCH[1]}"; done <file.txt

BASH_REMATCHявляется массивом, первый элемент которого является первой захваченной группой в сопоставлении Regex =~, в данном случае эпохой.


Если вы хотите сохранить структуру файла:

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

это выведет измененное содержимое в STDOUT, чтобы сохранить его в файле, например out.txt:

while ...; do ...; done >out.txt

Теперь, если вы хотите, вы можете заменить оригинальный файл:

mv out.txt file.txt

Пример:

$ 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
heemayl
источник
Хорошо .... что выводит конвертированную дату на экран, теперь, как мне получить эту команду, чтобы заменить записи в файле?
машинист
@machinist Проверьте мои правки ..
Heemayl
1
Если вы используете последнюю версию bash, printfможет сделать сам преобразование: printf '#%(%F %H)T\n' "${BASH_REMATCH[1]}".
chepner
14

Хотя это возможно с GNU sedс такими вещами, как:

sed -E 's/^#([0-9]+).*$/date -d @\1/e'

Это было бы ужасно неэффективно (и легко ввести произвольные уязвимости внедрения команд 1 ), поскольку это означало бы запуск одной оболочки и одной dateкоманды для каждой #xxxxстроки, практически так же плохо, как while readцикл оболочки . Здесь было бы лучше использовать такие вещи, как perlили gawk, то есть утилиты обработки текста, которые имеют встроенные возможности преобразования даты:

perl  -MPOSIX -pe 's/^#(\d+).*/ctime $1/se'

Или:

gawk '/^#/{$0 = strftime("%c", substr($0, 2))};1'

1 Если бы мы написали ^#([0-9]).*вместо ^#([0-9]).*$(как я делал в более ранней версии этого ответа), то в многобайтовых локалях, таких как UTF-8 (в настоящее время это норма), с вводом наподобие #1472047795<0x80>;reboot, где это <0x80>значение байта 0x80, которое не формирует допустимый символ, например, эта sкоманда должна была бы выполняться date -d@1472047795<0x80>; reboot. Хотя с дополнительным $, эти строки не будут заменены. Альтернативный подход: s/^#([0-9])/date -d @\1 #/eоставить часть после #xxxдаты в виде комментария оболочки

Стефан Шазелас
источник
1
Как насчет использования только одного экземпляраdate -f для выполнения всех преобразований в потоковом режиме?
Цифровая травма
Команда perl, кажется, добавляет новую строку после ctime $ 1, и я не могу найти способ удалить ее.
Алекс Харви
1
@Alex. Правильно. Смотрите редактировать. Добавление sфлага делает так, что .*также включает перевод строки при вводе. Вы также можете использовать strftime "%c", localtime $1.
Стефан
@ StéphaneChazelas большое спасибо. Это отличный ответ.
Алекс Харви
3

Все остальные ответы порождают новый dateпроцесс для каждой даты эпохи, которую необходимо преобразовать. Это может потенциально увеличить производительность, если ваш ввод велик.

Однако в GNU date есть удобная -fопция, которая позволяет одному экземпляру процесса dateнепрерывно читать входные даты без необходимости нового форка. Таким образом, мы можем использовать sed, pasteи dateтаким образом, чтобы каждый из них появлялся только один раз (2 раза за sed) независимо от того, насколько велик вход:

$ 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
$ 
  • Две sedкоманды, соответственно, в основном удаляют четные и нечетные строки ввода; первый также заменяется #на, @чтобы дать правильный формат метки времени эпохи.
  • Затем sedвыводится первый вывод, date -fкоторый выполняет необходимое преобразование даты для каждой полученной строки ввода.
  • Эти два потока затем вставляются в один требуемый выход с помощью paste. Эти <( )конструкции являются Баш процесс замены , которые эффективно Trick Пасты, думая , что читает из заданных имен файлов , когда это на самом деле читает вывод поступает из команды изнутри. -d '\n'говорит pasteразделить нечетные и четные выходные строки новой строкой. Вы можете изменить (или удалить) это, если, например, вы хотите, чтобы отметка времени находилась в той же строке, что и другой текст.

Обратите внимание, что в этой команде есть несколько GNUisms и Bashisms. Это не является Posix-совместимым и не должно быть переносимым за пределами мира GNU / Linux. Например, date -fделает что-то еще на OSXes dateвариант BSD .

Цифровая травма
источник
date -d(из вопроса) также непереносим ... (На FreeBSD он попытается связываться с настройками DST, на Solaris это выдаст ошибку ...) Хотя вопрос не определяет ОС ...
Герт ван ден Берг
@GertvandenBerg да, это рассматривается в последнем абзаце этого ответа.
Цифровая травма
Я имею в виду, что в образце аскера также есть проблемы с переносимостью ... (Вероятно, они должны были пометить ОС ...)
Герт ван ден Берг
1

Предполагая, что формат даты, который вы используете в своем сообщении, соответствует вашему желанию, следующее регулярное выражение должно соответствовать вашим потребностям.

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

Помните о том, что это заменит только одну эпоху на строку.

Hatclock
источник
Я получаю следующую ошибку с этой командой: sed: -e expression #1, char 48: invalid reference \3 on 's' command's RHS
машинист
1
Моя ошибка, отредактировал пост.
Hatclock
0

используя sed:

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

вывод :

ر أغس 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

так как мой язык локали арабский :)

Хассан
источник
0

Мое решение, как это сделать в конвейере

cat test.txt | sed 's/^/echo "/; s/\([0-9]\{10\}\)/`date -d @\1`/; s/$/"/' | bash
kayn
источник