На днях столкнулся с такой ситуацией: паттерн, который гарантированно должен обеспечивать непустой вывод, вместо текста производит множество пустых строк.
Уже сталкивался с этим, но давно и причину вспомнил не сразу. Пофрустрировав, всё-таки припомнил, разобрался детальнее и решил написать небольшую заметку.
TL;DR: Причина – встреча символа возврата каретки CR
(\r
) с управляющей последовательностью \x1B[K
: CR
перемещает курсор в начало строки, а \x1B[K
удаляет всё от курсора до конца строки. ОС – Ubuntu 24.04, терминал Terminator 2.1.3, шелл Bash.
Воспроизводим
Думая написать что-нибудь о разных способах извлечения информации из текстовых журналов, взял вот этот сэмпл системного лога Linux, чтобы на нём поупражняться:
https://raw.githubusercontent.com/logpai/loghub/refs/heads/master/Linux/Linux_2k.log
Из этого репозитория:
https://github.com/logpai/loghub
Выполнение следующей команды даёт интересный вывод:
grep -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log
Это признак того, что паттерн работает – в противном случае я сразу получил бы следующий промпт. По идее, я должен был получить 490 строк вывода:
$ grep -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log | wc -l
490
А если отправляю вывод в tail
, то его видно (забегая вперёд – это из-за того, что автоподсветка grep
отключает управляющие последовательности, когда вывод идёт в пайп):
$ grep -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log | tail -5
207.243.167.114 user=root
207.243.167.114 user=root
207.243.167.114 user=root
207.243.167.114 user=root
207.243.167.114 user=root
А также, если выполнить "захват" не до конца строки, а, скажем, всего три символа:
$ grep -Po 'authentication failure.*?rhost=\K...' Linux_2k.log
218
218
220
220
220
# ... и т.д.
Причина
Примеры логов хранятся в репозитории с разделителями строк стиля Windows – CRLF
(\r\n
) . Это последовательность символов возврата каретки (Carriage Return) и перевода строки (Line Feed). По работе я практически не сталкиваюсь с необходимостью что-то искать в таких файлах, да и дома тоже. А тут ещё и файл с системным логом Linux... Так что вывод file
сопроводился лёгким когнитивным диссонансом:
$ file Linux_2k.log
Linux_2k.log: ASCII text, with CRLF line terminators
Настало время посмотреть, что на самом деле происходит, когда я запускаю grep
:
$ alias | grep grep
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
Эти алиасы заданы в ~/.bashrc
:
$ grep grep ~/.bashrc
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
Включена автоподсветка:
Выключаем, и получаем ожидаемый вывод:
$ grep --color=never -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log
218.188.2.4
218.188.2.4
220-135-151-1.hinet-ip.hinet.net user=root
220-135-151-1.hinet-ip.hinet.net user=root
# ... и т.д.
Почему так происходит?
Для управления цветом и другими свойствами вывода в консоли Linux используются "управляющие последовательности" – текст, не отображающийся, а интерпретирующийся как команда:
https://www.man7.org/linux/man-pages/man4/console_codes.4.html
Т.е., при включении подсветки grep
может сгенерировать вывод так, что встреча с разделителем строк CRLF
приведёт к неожиданному поведению. С помощью cat -A
можно посмотреть на все символы вывода:
$ grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log \
| cat -A | tail -5
^[[01;31m^[[K207.243.167.114 user=root^M^[[m^[[K$
^[[01;31m^[[K207.243.167.114 user=root^M^[[m^[[K$
^[[01;31m^[[K207.243.167.114 user=root^M^[[m^[[K$
^[[01;31m^[[K207.243.167.114 user=root^M^[[m^[[K$
^[[01;31m^[[K207.243.167.114 user=root^M^[[m^[[K$
Пример выше с перенаправлением вывода в tail
, когда вывод становился видимым, работал из-за --color=auto
в алиасе – когда grep
пишет в консоль, он включает подсветку, а когда в пайп – выключает:
$ grep --color=auto -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log \
| cat -A | tail -5
207.243.167.114 user=root^M$
207.243.167.114 user=root^M$
207.243.167.114 user=root^M$
207.243.167.114 user=root^M$
207.243.167.114 user=root^M$
Разберём, что происходит в этой строке:
^[[01;31m^[[K207.243.167.114 user=root^M^[[m^[[K$
Знаком доллара cat -A
обозначает перевод строки LF
. Это стандартный разделитель строк в системах Linux, благодаря ему следующая строка выводится под текущей.
Остальные управляющие последовательности и символы указаны в каретной нотации:
https://ru.wikipedia.org/wiki/Каретная_нотация
Чтобы преобразовать их обратно в символы и последовательности, управляющие выводом в терминале, нужно заменить: ^[
на \x1B
, ^M
на \r
.
\x1B
это шестнадцатиричная репрезентация символа ESC
из таблицы ASCII:
https://www.ascii-code.com/
Этот символ начинает т.н. escape-последовательность, переводящую терминал в режим выполнения последующих команд. Его ещё обычно можно записывать, как \e
или в восьмеричной форме \033
. Следующий за ESC
символ [
маркирует начало введения управляющей последовательности (CSI – Control Sequence Introducer).
Когда после CSI встречается символ m
, мы имеем дело с SGR (Select Graphic Rendition) - параметрами, управляющими визуальными свойствами вывода. Эти параметры записываются между CSI (\x1B[
) и символом m
. Параметров может быть несколько, они разделяются символом ;
.
Итак, слева направо:
\x1B[01;31m
– это управляющая последовательность SGR, задающая два свойства: 01
– жирный шрифт, 31
– красный цвет текста.
\x1B[K
– указание терминалу удалить текст от позиции курсора до конца строки. Называется EL
(Erase Line).
После этого выводится, собственно, искомый текст – 207.243.167.114 user=root
.
\r
– возврат каретки CR
, указание терминалу перевести курсор в начало строки.
\x1B[m
– последовательность SGR без параметров интерпретируется как \x1B[0m
, reset, т.е. сброс визуальных параметров вывода к значениям по умолчанию. Без этой команды весь прочий текст продолжал бы выводится жирным шрифтом красного цвета.
\x1B[K
– и снова указание терминалу удалить текст от позиции курсора до конца строки. При выводе курсор находится там, куда будет печататься следующий отображаемый символ. Но \r
уже переместил курсор к началу строки, и до этого момента отображаемых символов не было, поэтому курсор остаётся там. А терминал, повинуясь \x1B[K
, удаляет весь текст – от начала до конца строки. Включая, разумеется, 207.243.167.114 user=root
.
Проделаем теперь замену каретной нотации, чтобы проверить:
$ grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log \
| cat -A \
| tail -5 \
| sed -E 's/\^\[/\x1B/g; s/\^M/\r/g'
$
$
$
$
$
$
Первый и последний знаки доллара – это промпт. Остальные – остатки вывода cat -A
, обозначающие перевод строки.
Обладая этой информацией, можно тривиально воспроизвести этот эффект без всяких логов, вручную:
$ echo -e "asdf\r\x1B[K"
$ echo -e "asdf\x1B[K"
asdf
$ echo -e "asdf\r\n\x1B[K"
asdf
$
И что с этим делать?
Можно преобразовать файл к разделителям строк LF
:
$ file Linux_2k.log
Linux_2k.log: ASCII text, with CRLF line terminators
$
$ grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log | tail -1
$ dos2unix Linux_2k.log
dos2unix: converting file Linux_2k.log to Unix format...
$
$ file Linux_2k.log
Linux_2k.log: ASCII text
$
$ grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log | tail -1
207.243.167.114 user=root
Правда, dos2unix
может не присутствовать по умолчанию. Но в популярных дистрибутивах она должна тривиально устанавливаться из репозитория.
Если по какой-то причине файл менять нельзя, то можно использовать переключатель grep --color=never ...
либо в отдельных командах, либо поправить алиасы в .bashrc
.
Ещё можно использовать переменную окружения GREP_COLORS=ne
:
$ unix2dos Linux_2k.log
unix2dos: converting file Linux_2k.log to DOS format...
$
$ grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log | tail -1
$
$ GREP_COLORS=ne grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log | tail -1
207.243.167.114 user=root
$
Эта возможность документирована в grep(1). Значение ne
– отменяет использование \x1B[K
:
$ GREP_COLORS=ne grep --color=always -Po 'authentication failure.*?rhost=\K.*' Linux_2k.log \
| cat -A | tail -1
^[[01;31m207.243.167.114 user=root^M^[[m$
Если почему-то нельзя трогать файл или установить dos2unix
, то я бы выбрал отмену подсветки, т.к. на практике особо ей не пользуюсь.
А зачем, вообще, там EL?
Как я понимаю, для легаси-терминалов и аномальных ситуаций, когда на строке вывода уже есть текст или по какой-то причине не сбрасывается цвет. Несколько попыток воспроизвести аномальное поведение результатов не дали.
Если интересно покопаться в этом, в grep-3.11/src/grep.c
, который можно взять отсюда – https://www.gnu.org/software/grep/ – есть об этом довольно длинный комментарий, начиная с 310-й строки.
Anguycat
Спасибо, теперь стало понятно, почему у меня не грепались логи
a1111exe Автор
Не за что!
Кстати, нагуглил, что в экосистеме Мак уже перешли с разделителей CR на LF. Эх, вот бы теперь и Майкрософт последовали этому примеру... :)