Описание: Работа с регулярными выражениями является частью повседневной работы системного администратора. Кандидат 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):
{[^{}]*} {([^{}]*|{[^{}]*})*} {([^{}]*|{([^{}]*|{[^{}]*})*})*} {([^{}]*|{([^{}]*|{([^{}]*|{[^{}]*})*})*})*}
Заметим, что начиная с некоторого уровня вложенности у этого регулярного выражения начнутся явные проблемы с производительностью, кроме того, существует некоторая критическая величина вложенности круглых скобок, при которой у интерпретатора окажется превышен лимит рекурсии.
Однако вернёмся к нашему регулярному выражению для поиска
«плохих» скриптов: /usr/bin/env +([^ ]+=[^ ]* +)*[^/][^=]+( |$)
.
Что если записать его попроще? Давайте сравним его с вот таким
регулярным выражением: /usr/bin/env +[^/]
. Мы убрали из
регулярного выражения весь его «ум». А теперь давайте
посмотрим правде в глаза: 1) этот ум был несовершенен
(например, он не учитывал ситуацию, когда вызов
env(1) и утилиты были бы записаны на разных
строках, через обратный слеш); 2) результат ухудшится не
более чем на 1-2%, так как ситуация, когда определяется какая-то
переменная редка.
Мораль: будьте проще и не зацикливайтесь на регулярных выражениях.
К сожалению, от программы к программе часто меняются не только возможности регулярных выражений, но и их синтаксис. Стало быть, нам понадобится некоторая сводная таблица.
В первом столбце этой таблицы приведён синтаксис языка программирования 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] | Инвертированный класс. Соответствие символу отсутствующему в указанном в наборе. В примере описано множество небукв |
Предопределённые классы (список неполон) | ||||
. | . | . | . | Любой символ |
\w | \w | [[:alnum:]_] | [[:alnum:]_] | Алфавитно-цифровой символ и подчерк (word) |
\W | \W | [^[:alnum:]_] | [^[:alnum:]_] | Множество дополнительное множеству word |
\d | \d | [[:digit:]] | [[:digit:]] | Цифры |
\D | \D | [^[:digit:]] | [^[:digit:]] | Не цифры |
\s | \s | [[:space:]] | [[:space:]] | Пробельные символы (пробел, табулятор, и т.п.) |
\S | \S | [^[: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 | \< | \< | \< | Левая граница слова |
\b | \> | \> | \> | Правая граница слова |
\B | Позиция не являющаяся границей слова | |||
(?=atom) | atom\@= | Заглядывание вперёд. Позиция за которой идёт atom. В vim(1) атом имеет право быть переменной длины, в perl(1) это не так. | ||
(?!atom) | atom\@! | Заглядывание вперёд. Позиция за которой нет atom'а | ||
(?<=atom) | atom\@<= | Заглядывание назад. Позиция перед которой есть atom | ||
(?<!atom) | atom\@<! | Заглядывание назад. Позиция перед которой нет atom'а | ||
При помощи заглядываний вперёд или назад можно
пытаться найти некоторый текст не содержащий заданного
слова. Например, шаблон <section>(.(?!<section\b))*</section>
ищет текст от <section> до
</section> , если
внутри него не случилось другого тега <section
| ||||
Или | ||||
| | \| | \| | | | Оператор «или» |
Группировка | ||||
() | \(\) | \(\) | () |
Группа: 1) ограничивает действие оператора
«или»: «Слава (КПСС|КПРФ) »
2) объединяет различные атомы вместе так, чтобы к
ним можно было применить общий квантификатор,
3) кроме того, впоследствии на текст
соответствующий группе можно ссылаться по номеру (все
группы последовательно нумеруются, а попавший в группу
текст запоминается).
|
(?:...) | \%(...\) | Ненумерующаяся группа | ||
\n | \n | \n | \n |
Ссылка на группу номер n: выражение ([a-z])\1 ищет удвоенные буквы.
Заметьте, это не то же саме, что [a-z]{2} , которое ище две
буквы, даже, если они неодинаковые.
|
Шаблоны встречающиеся в командной строке sh(1) тоже в некотором смысле являются регулярными выражениями.
Есть только одно обстоятельство, которое регулярным выражением так просто не записать: шаблоны Bourne shell не ищут совпадения с файлами начинающимися с точки, если это не указано явно.
В BSD установлен GNU grep(1).
Опции команды grep(1) можно ражделить на несколько типов: 1) синтаксические опции; 2) формат вывода: опции влияющие на характер выводимой информации; 3) опции влияющие на то где осуществляется поиск.
-E, --extended-regex
-F, --fixed-strings
-i, --ignore-case
-w, --word-regexp
grep -w
grep
найдёт слово grep, не не найдёт egrep.
-x, --line-regexp
-v, --invert-match
egrep -v '^(#|$)' test.sh
напечатает непустые строки, в которых нет комментария.
-v, --invert-match
egrep -v '^(#|$)' test.sh
напечатает непустые строки, в которых нет комментария.
-C [NUM], -NUM, --context[=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»)
-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
, означающее, что при наличии
совпадения будет выдано соответствующее сообщение из
одной строки.