7.14. Применение регулярных выражений

Описание:  Работа с регулярными выражениями является частью повседневной работы системного администратора. Кандидат BSDA должен быть способен искать текстовые шаблоны при анализе вывода программ или поиске в файлах. Кандидат должен уметь указать диапазон символов в скобках [], определить литерал (?!), использовать квантификаторы, отличать метасимволы и создавать инвертированные фильтры.

Практика: grep(1), egrep(1), fgrep(1), re_format(7).

Комментарий

Регулярные выражения это то, с чем администратор имеет дело почти ежеминутно. В этом вопросе безусловно нужна практика. Вместе с тем, чтобы полностью описать всё богатство возможностей регулярных выражений, все их недостатки, плюсы и минусы различных механизмов поиска, нужно потратить годы. К счастью, в нашем распоряжении есть перевод замечательной книги Дж. Фридла «Регулярные выражения» [Friedl-2001-ru]. К сожалению, издатели не планируют переиздавать её, однако любезно выложили для всеобщего использования текст этой книги. Вот небольшая цитата из предисловия к этой книге:

Небольшой тест — попробуйте определить понятие «между». Помните: определяемое слово не может использоваться в определении! Ну как, получилось? Нет? Действительно, задача не из простых. Хорошо, что смысл этого слова понятен всем, иначе нам пришлось бы подолгу разъяснять его всем несведущим. Даже такие простые концепции бывает трудно описать кому-то, кто еще не знаком с ними.

До определенной степени сказанное относится и к регулярным выражениям. На самом деле регулярные выражения не так сложны, как их описания и объяснения.

С другой стороны, положа руку на сердце, для задач администрироания не нужно прибегать ко всему богатству возможностей регулярных выражений. От администратора требуется лишь знание основных синтаксических конструкций, а в данном разделе я явно вышел за рамки того, что реально нужно в работе администратору. Web-программисту и, тем более, для верстальщику в LaTeX'е требуется больше.

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

Реальный пример: допустим вам нужно найти все IP-адреса, с которых на вашу машину пытались пройти используя несуществующие имена пользователей. Для этой цели вы можете изучить журнальный файл /var/log/auth.log разыскивая сообщения демона sshd, содержащие слова «Invalid user»:

$ awk '/sshd.*Invalid user/{print $10}' /var/log/auth.log | sort | uniq
125.243.235.194
193.158.246.173
218.248.33.225
  

Здесь команда awk(1) вырезает 10-й столбец из всех строк, которые соответствуют регулярному выражению sshd.*Invalid user. Конструкция .* означает сколько угодно чего угодно. Т.е. мы ищем в журнальном файле строку в которой написано sshd затем возможно какие-то ещё слова и цифры и Invalid user.

Это тривиальный пример, но такие тривиальные примеры рождаются каждую минуту. Он написан очень просто, хотя и не очень корректно. На реальном журнальном файле вероятность сбоя такого регулярного выражения исчисляется сотыми долями процента, так как сам журнальный файл написан автоматом и имеет строго определённый формат. Менее тривиальные примеры в администраторской практике появляются редко и нужны скорее при написании каких-то служебных программ. Например: найти на диске скрипты в которых утилита env(1) используется для вызова программ по неабсолютному пути. (Чем это может быть опасно рассматривается в Раздел 7.2.1.1, «env(1), printenv(1)».) Поскольку таких программ будет найдено во множестве, мы ограничились лишь первыми десятью в качестве примера.

$ find / -type f -perm +a+x -print0 2>/dev/null | 
> xargs -0 egrep '/usr/bin/env +([^ ]+=[^ ]* +)*[^/][^=]+( |$)' | head
/usr/local/lib/python2.4/test/pystone.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/re_tests.py:#!/usr/bin/env python
/usr/local/lib/python2.4/test/regrtest.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_al.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_array.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_binhex.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_bsddb.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_cd.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_cl.py:#! /usr/bin/env python
/usr/local/lib/python2.4/test/test_cmath.py:#! /usr/bin/env python
  

Регулярное выражение, написанное в аргументе команды egrep(1): /usr/bin/env +([^ ]+=[^ ]* +)*[^/][^=]+( |$) «переводится» на русский язык следующим образом: мы ищем текст /usr/bin/env, за которым возможно идёт несколько объявлений содержащих в себе знак равенства (т.е. объявление что некоторая переменная окружения равна какому-то значению), после которой идёт слово не содержащее в себе знак равенства и начинающееся не со знака / (т.е. не являющееся абсолютным путём). Разберём подробнее (если вы не знакомы с синтаксисом регулярных выражений, переходите к следующему подразделу, а потом вернитесь):

/usr/bin/env +
этот фрагмент ищет упоминание файла /usr/bin/env, за которым идёт не менее одного пробела.
([^ ]+=[^ ]* +)*
этот фрагмент ищет возможные присвоения значений переменным. Он требует наличия некоторого ненулевого количества непробелов, за которыми идёт знак равенства, а затем некоторое, возможно нулевое количество непробелов. таких объявлений может быть сколько угодно, от нуля до бесконечности.
[^/][^=]+( |$)
и наконец, здесь мы требуем наличия некоторого слова, которое начинается не со слеша, и не содержит в себе знаков равентсва, при этом состоит минимум из двух символов (читателю должно быть ясно, что под это описание странным образом попали слова начинающиеся со знака равенства...). После этого слова мы требуем наличия пробела, либо конца строки.

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

[Важно]Важно
Помните: регулярные выражения это удобный, но ненадёжный способ поиска. Как правило они ищут не то, что от них просят, а то, что удобнее искать.

Автор данного текста, однажды, по заданию издательства URSS, писал регулярное выражение для поиска фамилий и инициалов в тексте. Этот труд занял несколько дней. 90% времени ушло на тестирование и, в результате, полученный монстр работал с КПД не более 98%. В мире есть множество весьма необычных фамилий и вариантов написания инициалов, а так же разнообразных приставок вроде фон-, ван-дер-, де-, ибн- и т.п.

Многие регулярные выражения могут и должны писаться автоматически. Мне приходилось видеть работу регулярного выражения длиной 2 мегабайта. Ничего, живенько работало. Однако, это не прерогатива системного администратора. Просто, для программистов скажу, что если вы можете заменить регулярное выражение стековой машиной — сделайте это. Стековая машина работает намного корректнее и часто быстрее, хотя и требует большего количества телодвижений.

[Важно]Важно
Существуют задачи принципиально не имеющие решения в рамках механизма регулярных выражений. Например, поскольку регулярное выражение не может считать сколько конструкций оно захватило, с его помощью невозможно в общем виде решить задачу поиска ответной скобки, хотя и можно решить частный случай, когла известно, что глубина вложенности скобок не превышает n. В общем случае задача поиска ответной скобки, это задача для стековой машины.

Регулярное выражение для поиска ответной фигурной скобки при условии ограниченной вложенности скобок, написанное на perl(1):

{[^{}]*} 1
{([^{}]*|{[^{}]*})*} 2
{([^{}]*|{([^{}]*|{[^{}]*})*})*} 3
{([^{}]*|{([^{}]*|{([^{}]*|{[^{}]*})*})*})*} 4
  

1 Поиск ответной скобки при условии отсутствия вложенных скобок
2 Поиск ответной скобки при условии, что внутри них может быть некоторое количество вложенных пар скобок, но внутри оных ничего не вложено (т.е. глубина вложенности не более 1).
3 Поиск ответной скобки при условии, что глубина вложенности не превышает 2.
4 Поиск ответной скобки при условии, что глубина вложенности не превышает 3.

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

Однако вернёмся к нашему регулярному выражению для поиска «плохих» скриптов: /usr/bin/env +([^ ]+=[^ ]* +)*[^/][^=]+( |$). Что если записать его попроще? Давайте сравним его с вот таким регулярным выражением: /usr/bin/env +[^/]. Мы убрали из регулярного выражения весь его «ум». А теперь давайте посмотрим правде в глаза: 1) этот ум был несовершенен (например, он не учитывал ситуацию, когда вызов env(1) и утилиты были бы записаны на разных строках, через обратный слеш); 2) результат ухудшится не более чем на 1-2%, так как ситуация, когда определяется какая-то переменная редка.

Мораль: будьте проще и не зацикливайтесь на регулярных выражениях.

7.14.1. Диалекты регулярных выражений

К сожалению, от программы к программе часто меняются не только возможности регулярных выражений, но и их синтаксис. Стало быть, нам понадобится некоторая сводная таблица.

В первом столбце этой таблицы приведён синтаксис языка программирования perl(1). Этот диалект весьма распространён, встречается он и в других языках, например в python(1).

Второй столбец посвящён регулярным выражениям редактора vim(1). Не путайте его с vi(1). Возможности vi(1) намного скромнее. По поводу vim(1) хочется заметить вот что: здесь не перечислено и половины его возможностей. Это самый богатый диалект регулярных выражений, который я знаю, даже богаче perl(1). Но это же и самый медленный диалект.

grep(1) и egrep(1) представленные в третьем и четвёртом столбцах, являются одной и той же программой. egrep(1) это grep(1) вызванный с опцией -E, и хотя в man(1) сказано, что эта опция включает расширенные регулярные выражения, ничего она не включает, это не более чем переключатель синтаксиса (см. ниже).

Синтаксис регулярных выражений grep(1) совпадает с синтаксисом таких утилит, как sed(1) или awk(1), так как они слинкованы с той же библиотекой регулярных выражений, и описан в re_format(7), хотя и весьма не наглядно. (Точнее, синтаксис sed(1) совпадает с синтаксисом grep(1), а синтаксис регулярных выражений awk(1) совпадает с синтаксисом egrep(1).) Кроме того, поскольку эти программы вызываются из разнообразных скриптов, (например apropos(1), это скрипт Bourne shell), то регулярные выражения grep(1) реально используются много где, даже если в документации про это ничего не сказано.

Таблица 7.14. Регулярные выражения. Сводная синтаксическая таблица

perl(1)vim(1)grep(1)egrep(1)Описание
Классы
[a-zA-Z][a-zA-Z][a-zA-Z][a-zA-Z]Класс. Соответствие символу указанному в наборе, можно указывать диапазоны. В примере описано множество букв
[^a-zA-Z][^a-zA-Z][^a-zA-Z][^a-zA-Z]Инвертированный класс. Соответствие символу отсутствующему в указанном в наборе. В примере описано множество небукв
Предопределённые классы (список неполон)
....Любой символ
ww[[:alnum:]_][[:alnum:]_]Алфавитно-цифровой символ и подчерк (word)
WW[^[:alnum:]_][^[:alnum:]_]Множество дополнительное множеству word
dd[[:digit:]][[:digit:]]Цифры
DD[^[:digit:]][^[:digit:]]Не цифры
ss[[:space:]][[:space:]]Пробельные символы (пробел, табулятор, и т.п.)
SS[^[:space:]][^[:space:]]Непробельные символы символы
Квантификаторы «жадные»
****Повтор предыдущего символа 0 и более раз
++++Повтор предыдущего символа 1 и более раз
????Повтор предыдущего символа 0 или 1 раз
{n,m}{n,m}{n,m}{n,m}Повтор предыдущего символа от n до m раз
{n,}{n,}{n,}{n,}Повтор предыдущего символа минимум n раз
{n}{n}{n}{n}Повтор предыдущего символа строго n раз
Квантификаторы «нежадные»
*?{-}  Повтор предыдущего символа 0 и более раз, но как можно меньше
+?{-1,}  Повтор предыдущего символа 1 и более раз, но как можно меньше
{n,m}?{-n,m}  Повтор предыдущего символа от n до m раз, но как можно меньше
{n,}?{-n,}  Повтор предыдущего символа минимум n раз, но как можно меньше
Специальные позиции
^^^^Начало строки
$$$$Конец строки
<<<Левая граница слова
>>>Правая граница слова
B   Позиция не являющаяся границей слова
(?=atom)atom@=   Заглядывание вперёд. Позиция за которой идёт atom. В vim(1) атом имеет право быть переменной длины, в perl(1) это не так.
(?!atom)atom@!   Заглядывание вперёд. Позиция за которой нет atom'а
(?<=atom)atom@<=   Заглядывание назад. Позиция перед которой есть atom
(?<!atom)atom@<!   Заглядывание назад. Позиция перед которой нет atom'а
При помощи заглядываний вперёд или назад можно пытаться найти некоторый текст не содержащий заданного слова. Например, шаблон <section>(.(?!<section))*</section> ищет текст от <section> до </section>, если внутри него не случилось другого тега <section
Или
||||Оператор «или»
Группировка
()()()() Группа: 1) ограничивает действие оператора «или»: «Слава (КПСС|КПРФ)» 2) объединяет различные атомы вместе так, чтобы к ним можно было применить общий квантификатор, 3) кроме того, впоследствии на текст соответствующий группе можно ссылаться по номеру (все группы последовательно нумеруются, а попавший в группу текст запоминается).
(?:...)\%(...)  Ненумерующаяся группа
Ссылка на группу номер n: выражение ([a-z])1 ищет удвоенные буквы. Заметьте, это не то же саме, что [a-z]{2}, которое ище две буквы, даже, если они неодинаковые.

Шаблоны встречающиеся в командной строке sh(1) тоже в некотором смысле являются регулярными выражениями.

Таблица 7.15. Регулярные выражения и шаблоны командной Bourne shell

sh(1)perl(1)
*.*
?.
[][]
[^][^]

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

7.14.2. Возможности команды grep(1)

В BSD установлен GNU grep(1).

Опции команды grep(1) можно ражделить на несколько типов: 1) синтаксические опции; 2) формат вывода: опции влияющие на характер выводимой информации; 3) опции влияющие на то где осуществляется поиск.

7.14.2.1. Синтаксические опции

-E, --extended-regex
Синтаксис регулярных выражений переключается в «perl'оподобный» режим. Эквивалентно вызову программы egrep(1).
-F, --fixed-strings
Регулярные выражения отключаются, ведётся поиск простого текста. Эквивалентно вызову программы fgrep(1).
-i, --ignore-case
Сделать поиск нечувствительным к регистру
-w, --word-regexp
Искать целые слова. Команда grep -w grep найдёт слово grep, не не найдёт egrep.
-x, --line-regexp
Искать целые строки.
-v, --invert-match
Инвертировать условие, т.е. печатать только те строки, которые не соответствуют заданному условию. Пример: команда egrep -v '^(#|$)' test.sh напечатает непустые строки, в которых нет комментария.

7.14.2.2. Опции влияющие на формат выводимой информации

-v, --invert-match
Инвертировать условие, т.е. печатать только те строки, которые не соответствуют заданному условию. Пример: команда egrep -v '^(#|$)' test.sh напечатает непустые строки, в которых нет комментария.
-C [NUM], -NUM, --context[=NUM]
Выводить NUM строк перед строкой с найденным шаблоном и после. Т.е. задать вывод «контекста».
-A NUM, --after-context=NUM, -B NUM, --before-context=NUM
отдельно задаётся размер контекста после найденной строки и перед ней.
-c, --count
Вместо нормального вывода, печатается количество вхождений в тот или иной файл.
-H, --with-filename
Для каждого вхождения печатать имя файла
-h, --no-filename
Не печатать имя файла
-l, --files-with-matches
Печатать только имена файлов, в которых найден шаблон.
-L, --files-without-matches
Печатать только имена файлов, в которых не найден шаблон.
-b, --byte-offset
Печатать смещение в байтах от начала файла до найденного фрагмента.
-n, --line-number
Печатать номер строки в которой найден шаблон.
-q, --quiet, --silent, -s, --no-messages
Ничего не печатать. -q подавляет вывод информации на STDOUT, а -s на STDERR. В man(1) содержится рекомендация не использовать эти опции при написании скриптов, которые должны быть абсолютно переносимыми, пользуясь вместо них обычным перенаправлением вывода.
--null
Разделять информацию нулевым символом. Работает подобно опции -print0 команды find(1) и предназначено для ассоциации с командой xargs(1). (см. Раздел 7.15, «Преодоление ограничений на длину командной строки» и Раздел 7.6.3, «Связка с командой xargs»)

7.14.2.3. Опции влияющие на то, где осуществляется поиск

-r, --recursive
Рекурсивно искать во всех файлах в подкаталогах. Эквивалентно опции -d recurse. Возможно более разумно пользоваться связкой find-xargs-grep, во всяком случае, это надёжнее. Если вас посетила идея искать слово во всех файлах компьютера, то команда find / -type f -print0 | xargs -0 grep pattern имеет шансы успешно завершиться, чего не приходится ожидать от команды grep pattern -r / — такая команда в лучшем случае повиснет. Опцию -r имеет смысл применять лишь на небольших файловых иерархиях, про которые вы можете уверенно сказать, что в них нет симлинков ведущих наружу, а так же файлов устройств.
-d ACTION, --directories=ACTION
Как вести себя с каталогами. Если ACTION равно skip, ничего не делать, если read — искать в них как в файлах, если recurse, grep(1) рекурсивно ищет во всех файлах, встретившихся в данном каталоге и во всех подкаталогах. См. так же замечание к предыдущей опции.
-a, --text, --binary-files=text
Искать в бинарных файлах как будто они текстовые.
-I, --files-without-match, --binary-files=without-match
Не искать в бинарных файлах.
--binary-files=TYPE
Вести себя с бинарными файлами в соответствии с опцией TYPE. Значения text и without-match только что были описаны, по умолчанию используется значение binary, означающее, что при наличии совпадения будет выдано соответствующее сообщение из одной строки.

7.14.2.4. Прочие опции

-e PATTERN, --regexp=PATTERN
Эту опцию полезно применять, если шаблон на чинается со знака -. А вообще-то она необязательна: первая «не опция» считается шаблоном, а остальное — именами файлов и каталогов.
--help
Помощь
-V, --version
Номер версии

Обновлено: 12.03.2015