Регулярные выражения для grep
Зачем это нужно
В главе про терминал мы обходились командами над файлами целиком: показать, скопировать, удалить. Но на чемпионате и в работе постоянно нужна операция потоньше — найти в большом файле нужные строки. Лог веб-сервера на десять тысяч строк, в котором нужно выловить только ошибки. Конфиг, в котором нужно найти строку с портом. Чужой проект, в котором нужно понять, где вообще используется функция с таким-то именем. Листать глазами — не вариант; для этого есть grep.
grep расшифровывается как global regular expression print — «напечатать по глобальному регулярному выражению». Название выдаёт главное: сила grep не в поиске точного текста (это он тоже умеет), а в поиске по шаблону — регулярному выражению (regular expression, коротко regex). Шаблон описывает не конкретную строку, а правило: «строка, начинающаяся с цифры», «слово error в любом регистре», «три цифры, дефис, ещё две цифры». Один раз научившись читать и писать такие правила, ты получаешь суперспособность, которая работает далеко за пределами grep: те же регулярные выражения понимают редакторы кода, языки программирования и половина инструментов командной строки.
Эта страница — углубление: базовый маршрут от неё не зависит, но перед ней стоит пройти главу «Linux и терминал», чтобы команды, флаги и конвейер | уже не были новостью.
- Зачем это нужно
- grep без регулярок: точный текст и главные флаги
- Точка, экранирование и классы символов
- Якоря: начало и конец строки
- Квантификаторы: сколько раз повторить
- Жадность и типичные ошибки новичков
- Группы, обратные ссылки и границы слов
- PCRE вкратце: \d, \w и просмотр вперёд
- grep -F: когда регулярка не нужна вообще
- Контекст вокруг совпадения и поиск по дереву проекта
- Живые сценарии: зачем это в реальной работе
- grep в скриптах: код возврата и другие мелочи
- Несколько шаблонов сразу и разбор прод-инцидента
- Практика: grep-квест в терминале
- Шпаргалка
- Что должен уметь
- Куда дальше: проверенные ресурсы
grep без регулярок: точный текст и главные флаги
Начнём с того, что regex ещё не требует. В простейшей форме grep получает текст для поиска и файл — и печатает все строки, в которых этот текст встречается:
grep error app.log
Нажми на части команды посложнее — с флагами, которые понадобятся в первую очередь:
Флаги, без которых не живёт ни один день с grep:
-i— игнорировать регистр. Ошибки в логах пишут кто во что горазд:Error,ERROR,error— с-iнайдутся все.-n— показывать номера строк. Нашёл строку — сразу знаешь, куда идти в редакторе.-v— инвертировать поиск: напечатать строки, где шаблона нет.grep -v '#' config.txt— конфиг без строк с решёткой.-c— не печатать строки, а посчитать их:grep -c error app.logответит одним числом.-w— искать только целые слова:grep -w logнайдётlog, но неloginи неcatalog.-r— искать рекурсивно по всем файлам каталога:grep -r TODO src/— все заметки TODO во всём проекте.
И приём из главы про терминал: grep умеет читать не только файлы, но и вывод другой команды через конвейер — cat app.log | grep error даёт тот же результат, а history | grep ssh найдёт все команды ssh, которые ты набирал.
Точка, экранирование и классы символов
Теперь сама магия. В регулярном выражении часть символов — не буквы, а операторы. Первый из них — точка: . означает «ровно один любой символ». Шаблон v.sion найдёт и vision, и version... стоп, нет: version не найдёт — между v и sion там два символа. А вот v..sion — найдёт version, потому что двум точкам соответствуют er.
Отсюда сразу неприятный сюрприз: если искать точку буквально — например, версию 1.5 или расширение .txt — шаблон 1.5 найдёт заодно 105, 1x5 и вообще «1, что угодно, 5». Чтобы оператор снова стал обычным символом, его экранируют обратной косой чертой: 1\.5 — это уже строго «единица, точка, пятёрка».
Второй оператор — класс символов: квадратные скобки со списком допустимых символов внутри. [abc] — «один символ: a, b или c». Внутри класса работает диапазон через дефис: [0-9] — любая цифра, [a-z] — любая строчная латинская буква, [A-Za-z0-9] — буква или цифра. А если первый символ в скобках — крышка, класс инвертируется: [^0-9] — «один любой символ, кроме цифры».
Обрати внимание на кавычки вокруг шаблона. Символы *, [, ], $ — операторы не только для grep, но и для самой оболочки bash (вспомни подстановку *.txt из главы про терминал). Одинарные кавычки говорят оболочке: «не трогай, передай как есть». Правило простое: шаблон со спецсимволами — всегда в одинарных кавычках; за привычку брать в кавычки вообще любой шаблон никто не осудит.
Якоря: начало и конец строки
По умолчанию grep ищет совпадение где угодно в строке. Якоря (anchors) прибивают шаблон к месту: ^ — к началу строки, $ — к концу.
grep '^From' mail.txt— строки, начинающиеся сFrom; письмо, гдеFromвстречается в середине текста, не подойдёт.grep 'done$' tasks.txt— строки, заканчивающиеся наdone.grep '^$' file.txt— пустые строки: начало строки, сразу за ним конец, между ними ничего. Аgrep -c '^$' file.txtих посчитает.grep -v '^#' config.txt— фильтр комментариев поточнее, чем раньше: выкидываются только строки, где#стоит первым символом, а строки, где решётка встречается в середине, остаются.
Якорь — не символ строки, а позиция: ^ не ищет никакую «крышку» в тексте, он утверждает «здесь начало». Поэтому шаблон ^$ находит строку нулевой длины, хотя в нём два символа.
Квантификаторы: сколько раз повторить
Последний кирпич — квантификаторы (quantifiers), операторы «сколько раз повторяется предыдущий элемент»:
*— ноль или больше раз.ab*cнайдётac,abc,abbbbc.+— один или больше раз.ab+cнайдётabcиabbbc, но уже неac.?— ноль или один раз.colou?rнайдёт иcolor, иcolour.{n}— ровно n раз,{n,m}— от n до m раз.[0-9]{4}— ровно четыре цифры подряд.
Квантификатор относится к одному предыдущему элементу — символу или классу. [0-9]+ — «одна или больше цифр», а 10* — это «1, а потом сколько угодно нулей» (10, 100, 1000...), не «десять сколько-то раз».
Тонкость, о которую спотыкаются все: исторически у grep два диалекта регулярных выражений. В базовом (BRE — basic regular expressions), который включён по умолчанию, операторы +, ?, {} нужно писать с обратной косой: \+, \?, \{4\}. В расширенном (ERE — extended regular expressions) они работают как есть, но нужен флаг -E. Проще всего запомнить одно правило: пользуешься +, ? или фигурными скобками — добавляй -E и пиши по-человечески.
Жадность и типичные ошибки новичков
Здесь стоит остановиться и разобрать грабли, на которые наступают почти все, кто только начал писать шаблоны.
Точка вместо экранированной точки. Уже разбирали выше, но повторим отдельно, потому что это самая частая ошибка вообще: grep 192.168.1.1 file найдёт не только настоящий IP, но и 192x168y1z1 — мало ли что случайно окажется на месте точек в другом контексте. Правило простое: если символ в шаблоне должен означать ровно этот символ, а не оператор — экранируй. Для IP это особенно коварно: 192.168.1.1 выглядит невинно, но незаэкранированные точки делают шаблон куда шире, чем кажется на глаз.
Жадность * и .*. Квантификатор * (и +) — жадный (greedy): он захватывает максимально длинный кусок текста, какой ещё позволяет найти совпадение целиком. grep -o '".*"' data.txt в строке "a", "b", "c" найдёт не три отдельные пары кавычек, а одну — "a", "b", "c" целиком, потому что .* растягивается от первой кавычки до последней в строке, а не до ближайшей. Новичок ждёт «сначала кавычка, что угодно, снова кавычка — покороче», а получает «всё, что нашлось между самой первой и самой последней кавычкой».
.* — это не «любой текст между двумя метками», а «максимально длинный текст, при котором шаблон ещё матчится». В строке с несколькими одинаковыми разделителями жадность почти всегда даёт не тот результат, который ты ожидал. Проверяй на реальных данных, а не только в голове.
В обычном grep укротить жадность нечем — ленивый (non-greedy, *?) квантификатор в базовом и расширенном диалектах не существует. Он появляется только в PCRE, то есть с флагом -P: grep -oP '".*?"' data.txt на той же строке уже честно найдёт "a", "b" и "c" по отдельности — *? берёт минимально возможный кусок. Если -P недоступен, спасаются классом-исключением: grep -o '"[^"]*"' data.txt — «кавычка, затем сколько угодно символов, которые не кавычка, затем кавычка» — работает без -P и даёт тот же результат, потому что явно запрещает квантификатору перепрыгивать через границу.
Регистр по умолчанию учитывается. grep Error log не найдёт error и не найдёт ERROR — регистр важен, пока не добавлен -i. Логи и код пишут вперемешку, и забытый -i — причина половины «команда ничего не нашла, хотя я точно видел эту строку».
BRE и ERE — не варианты одного языка, а разные грамматики. Причина исторической путаницы в том, что POSIX закрепил два набора правил для одних и тех же символов. В BRE операторы +, ?, |, {}, круглые скобки группировки без косой черты — это обычные символы: grep 'a{3}' file в базовом диалекте ищет буквально текст a{3} (с фигурными скобками!), а вовсе не «три буквы a». В ERE ровно наоборот: без косой черты это операторы, а с косой чертой (\{, \+) они становятся обычным текстом. Это не баг и не недосмотр — так исторически развивались две ветки одного стандарта, и grep до сих пор поддерживает обе ради обратной совместимости со скриптами 1980-х. Практический вывод уже звучал: определился, что нужны +/?/{}/группы — сразу пиши -E и не путайся, какие символы куда экранировать.
Группы, обратные ссылки и границы слов
Круглые скобки в ERE (grep -E) — не только группировка приоритета, но и способ захватить часть совпадения, чтобы сослаться на неё дальше в этом же шаблоне. Такая ссылка называется обратной (backreference) и пишется \1, \2 — номер по порядку открывающих скобок.
Классика применения — найти повторяющееся слово: grep -E '\b([a-z]+) \1\b' text.txt найдёт строки вроде the the cat — где одно и то же слово написано подряд дважды. Здесь ([a-z]+) захватывает слово в группу 1, а \1 требует, чтобы дальше в строке встретилось точно то же самое, что попало в группу — не просто «ещё одно слово из строчных букв», а именно повтор.
Обратные ссылки работают и в BRE — там группы пишутся \(...\), а ссылка всё равно \1: grep '\(rm -rf\) .*\1' — надуманный пример, но принцип тот же.
\b — ещё один якорь, только не «начало/конец строки», а граница слова: место, где буквенно-цифровой символ соседствует с небуквенным (или с началом/концом строки). grep -w log из первой главы делает примерно то же самое, но \b гибче — им можно ограничить только один край: grep 'log\b' не найдёт login, но найдёт catalog (там log не в начале слова, а \b стоит только справа). GNU grep понимает \b во всех диалектах, хотя формально это расширение, а не часть POSIX.
POSIX-классы [[:alpha:]], [[:digit:]], [[:alnum:]], [[:space:]], [[:upper:]], [[:lower:]] — работают только внутри квадратных скобок, замена диапазонам вроде [a-z]. Разница не только в длине записи: диапазон [a-z] — это буквально «байты между кодом a и кодом z» в текущей локали, а [[:alpha:]] — «то, что данная локаль считает буквой». На системе с русской локалью [[:alpha:]] подхватит и кириллицу, а [a-z] — никогда, что бы ни стояло в LANG. Для чемпионатных задач, где ввод почти всегда латиница и цифры, разница не критична, но привычка пригодится на реальных проектах с интернационализацией.
Комбинация квадратных скобок тройная: внешние [...] — сам класс символов, внутренние [: :] — обёртка имени POSIX-класса. [[:digit:]] читается как «класс, содержащий POSIX-класс digit» — непривычно на вид, но это не опечатка.
PCRE вкратце: \d, \w и просмотр вперёд
С флагом -P grep переключается на Perl-совместимые регулярные выражения — тот же диалект, что лежит в основе regex в Python, JavaScript, Java и большинстве современных языков. Разница с BRE/ERE не только в сокращениях, но и в целом наборе возможностей, которых у POSIX-диалектов просто нет.
Сокращённые классы. Вместо [0-9] и [[:digit:]] можно писать \d — «цифра». Вместо [A-Za-z0-9_] — \w — «символ слова» (буква, цифра или подчёркивание). \s — пробельный символ, короткая замена [[:space:]]. Заглавная версия инвертирует: \D — не цифра, \W — не символ слова, \S — не пробел.
grep -oP '\d{3}-\d{2}-\d{2}' codes.txt
Тот же результат, что у grep -E '[0-9]{3}-[0-9]{2}-[0-9]{2}', но короче — и это уже тот самый синтаксис, который повторится в коде на Python или JS.
Ленивые квантификаторы. Уже встречались выше: *?, +?, ?? — «минимально возможное количество повторений» вместо максимального. Работают только с -P.
Просмотр вперёд (lookahead). (?=...) — «дальше должно идти то-то, но само оно не входит в совпадение». Например, найти число, за которым сразу следует знак %, но захватить только само число, без процента:
grep -oP '\d+(?=%)' report.txt
В строке CPU: 87%, RAM: 42% это вернёт 87 и 42 — без символа %, потому что просмотр вперёд проверяет следующий символ, не включая его в результат. Есть и отрицательный просмотр вперёд (?!...) — «дальше НЕ должно идти то-то» — и симметричные просмотры назад (?<=...), (?<!...).
-P — не часть переносимого POSIX-набора: он есть в GNU grep (Linux) и не гарантирован на других системах (например, в BSD-версии grep на macOS его нет по умолчанию). Для учебных задач и на своей машине пользуйся смело, но в скрипте, который должен работать где угодно, закладывайся на BRE/ERE или ставь ripgrep/GNU grep явно.
grep -F: когда регулярка не нужна вообще
Иногда шаблон, который нужно найти, сам по себе набит символами, которые regex считает операторами — например, вызов функции calculate(x, y) или путь C++/std::vector. Экранировать каждую скобку и точку вручную — источник опечаток на ровном месте. Для этого есть -F (fixed strings, то же самое, что устаревшая команда fgrep): он выключает интерпретацию regex полностью, и весь шаблон читается буквально, символ в символ.
grep -F 'calculate(x, y)' src/*.js
Без -F пришлось бы писать grep 'calculate(x, y)' и — сюрприз — это тоже сработало бы, потому что круглые скобки в BRE без экранирования не операторы (это верно только для базового диалекта!). Но стоило бы кому-то в команде привычно добавить -E для другого шаблона по соседству и скопировать этот же поиск — как скобки внезапно превратились бы в группировку, и совпадение сломалось бы без видимой причины. -F снимает вопрос полностью: с ним неважно, сколько в шаблоне скобок, точек и звёздочек — всё воспринимается буквально, при любом диалекте и при любом флаге рядом.
-F — это ещё и самый быстрый режим grep: без разбора regex поиск подстроки работает заметно быстрее на очень больших файлах, если шаблон заведомо не содержит операторов.
Правило простое: если шаблон — точный текст без единого regex-оператора (имя функции, точный путь, готовая строка ошибки, скопированная из лога) — -F избавляет от риска экранирования и работает быстрее. Если в шаблоне нужна хоть одна метасимвольная возможность regex — точка как «любой символ», класс, квантификатор — -F не подходит, он и это прочитает буквально.
Контекст вокруг совпадения и поиск по дереву проекта
До сих пор grep печатал ровно совпавшую строку. На практике часто нужна пара строк вокруг неё — увидеть, в каком блоке кода или лога находится совпадение:
-A N(after) — N строк после совпадения.-B N(before) — N строк до совпадения.-C N(context) — по N строк с обеих сторон разом.
grep -C 2 'panic' service.log
Это покажет каждую строку с panic вместе с двумя строками до и двумя после — обычно этого хватает, чтобы понять, что происходило вокруг сбоя, не открывая лог целиком.
Ещё пара флагов, без которых не обойтись при работе с реальным репозиторием, а не с одним файлом:
-l(list) — вместо самих строк напечатать только имена файлов, где шаблон нашёлся хотя бы раз.grep -rl TODO src/— список файлов с заметками TODO, без самих заметок.-L— наоборот, файлы, где шаблон не встретился ни разу.--include='*.py'/--exclude='*.min.js'— при рекурсивном поиске (-r) ограничить, какие файлы вообще смотреть по маске имени.grep -r --include='*.js' --exclude-dir=node_modules TODO .— искать TODO только в.js-файлах, не заходя вnode_modules.--exclude-dir=DIR— та же идея, но по каталогам;.gitиnode_modulesв рекурсивном поиске почти всегда стоит исключать явно.
Живые сценарии: зачем это в реальной работе
Регулярки ради регулярок быстро надоедают. Вот пять ситуаций, где grep реально экономит время — примерно то, чем придётся заниматься на чемпионате и потом на работе.
1. Собрать все TODO и FIXME по проекту. Классика: разработчики оставляют заметки прямо в коде, и раз в спринт их нужно собрать в один список.
grep -rn -E 'TODO|FIXME' --include='*.js' --include='*.py' src/
-E 'TODO|FIXME' — оператор | (тоже требует -E) означает «или»: строка подходит, если содержит любую из альтернатив. -n сразу даёт номер строки, чтобы не искать глазами.
2. Найти все места, где используется функция, перед рефакторингом. Прежде чем переименовать или изменить сигнатуру функции, нужно понять, где она вообще вызывается — иначе легко что-то сломать втихую.
grep -rn '\bcalculateTotal(' --include='*.js' src/
\b перед именем не даёт зацепить recalculateTotal( — важно, когда имя функции может быть подстрокой другого имени. Открывающая скобка в конце шаблона отсекает случаи, где слово calculateTotal встретилось в комментарии без вызова.
3. Найти вероятную утечку секрета в коде. Один из самых частых типов уязвимостей — забытый в коде API-ключ или пароль. Прежде чем коммитить, стоит прогнать грубую проверку:
grep -rniE '(api[_-]?key|secret|password|token)\s*=\s*["\047][a-z0-9]{16,}["\047]' --include='*.js' --include='*.py' .
Смысл: ищем строку вида api_key = "AKIA1234567890ABCDEF" — слово-маркер (api_key, secret, password, token, регистр любой из-за -i), затем знак равенства, затем длинная строка в кавычках. \047 — восьмеричный код одинарной кавычки: так её можно вставить внутрь шаблона, который сам взят в одинарные кавычки, не разрывая их раньше времени. Это грубый эвристический фильтр, а не гарантия — он даст ложные срабатывания и пропустит хитрые случаи, но как быстрый предохранитель перед git push вполне годится. Для серьёзной проверки существуют специализированные инструменты (gitleaks, trufflehog) — они умеют то же самое надёжнее.
Никогда не проверяй такой шаблон на реальных боевых ключах в общем логе или чате — сам факт, что строка «похожа на секрет», уже повод её ротировать, если она куда-то утекла. Здесь и везде на платформе примеры ключей — вымышленные.
4. Найти файлы по содержимому, а не по имени, и что-то с ними сделать. grep находит строки, но часто нужно передать файлы дальше — например, в sed для массовой замены. Связка grep -l + xargs:
grep -rl 'oldFunctionName' --include='*.js' src/ | xargs grep -c 'oldFunctionName'
Здесь grep -rl выдаёт список файлов, где встречается oldFunctionName, а xargs подставляет каждое имя файла как аргумент второй команде — которая уже считает, сколько раз имя встретилось в каждом файле. Тот же приём работает с find: find . -name '*.log' | xargs grep -l ERROR — сначала find отбирает файлы по маске имени, потом grep фильтрует их по содержимому.
5. Быстро прикинуть уровень ошибок в проде. Утилита -c вместе с конвейером даёт мгновенную сводку:
grep -c ERROR *.log
Печатает по одной строке на файл: имя файла и количество ошибок в нём — без единого лишнего действия.
grep в скриптах: код возврата и другие мелочи
Когда grep вызывается не руками, а из другого скрипта, важны детали, которые в интерактивной работе почти не замечаешь.
Код возврата. grep не только печатает строки — он ещё и возвращает код завершения (exit status): 0, если совпадение нашлось, 1, если не нашлось, 2, если случилась ошибка (например, файла не существует). Это делает grep готовым строительным блоком для условий в скриптах:
if grep -q 'ERROR' service.log; then
echo "В логе есть ошибки"
fi
Флаг -q (quiet) — важная деталь: он подавляет обычный вывод grep и оставляет только код возврата. Без -q команда сработала бы так же с точки зрения if, но заодно напечатала бы в терминал саму найденную строку — лишний шум, если тебе нужен только факт «нашлось / не нашлось».
-f pattern-file — шаблоны для поиска можно держать не в командной строке, а в отдельном файле, по одному на строку: grep -f patterns.txt log.txt найдёт строки, подходящие под любой из шаблонов файла. Удобно, когда список того, что нужно найти, длинный или пересобирается динамически другим скриптом.
-H и -h — управляют тем, показывать ли перед каждой строкой имя файла. Если передать grep несколько файлов, он сам добавляет префикс имя_файла: перед каждой строкой; -h этот префикс убирает (полезно, когда файл всё равно один и имя не нужно), -H — принудительно включает его, даже если файл всего один (полезно в скриптах, где вывод потом парсится и формат должен быть предсказуемым независимо от числа файлов).
Бинарные файлы. По умолчанию grep, наткнувшись на файл, который выглядит не текстовым (например, скомпилированный .so или картинка), не станет вываливать в терминал мусор — он напечатает binary file matches и перейдёт дальше. Если действительно нужно искать текст внутри бинарного файла построчно как есть — -a заставляет grep обращаться с ним как с текстом, а -I в рекурсивном режиме, наоборот, полностью пропускает бинарные файлы, даже не сообщая о совпадении.
Привычка проверять код возврата, а не парсить вывод команды глазами — общий принцип автоматизации, не только для grep. grep -q ... && действие — компактная замена if, которая часто встречается в однострочниках и хуках.
Несколько шаблонов сразу и разбор прод-инцидента
Ещё один частый вопрос: как искать «это ИЛИ то», не переключаясь в ERE. Два способа, разного вкуса.
-e — несколько независимых шаблонов в одной команде. grep -e ERROR -e WARN service.log найдёт строки с любым из двух шаблонов — фактически то же самое, что grep -E 'ERROR|WARN', но без переключения диалекта. Удобно, когда шаблоны и так пишутся по одному через -f или собираются скриптом.
\| в базовом диалекте. У GNU grep альтернатива | как GNU-расширение работает даже в BRE, если её экранировать: grep 'ERROR\|WARN' service.log. Это не стандарт POSIX (переносимость под вопросом), но на практике встречается часто — стоит уметь узнавать в чужом коде, даже если сам предпочитаешь -E.
Теперь соберём всё в один связный пример — как это обычно и происходит в работе: не «примени вот этот оператор», а «разберись, что случилось». Допустим, продовый сервис упал ночью, и в service.log за сутки — сотни тысяч строк. Задача: понять, что произошло, за несколько команд, не открывая файл целиком.
Шаг 1. Прикинуть масштаб. Сначала не читаем строки, а считаем:
grep -c -E 'ERROR|CRITICAL' service.log
Допустим, вышло 42 — не единичный сбой, а что-то системное.
Шаг 2. Понять, когда всё началось. Флаг -n даёт номера строк, а head/tail из главы про терминал сузят вывод до первых находок:
grep -n -E 'ERROR|CRITICAL' service.log | head -5
Первая найденная строка — это примерно момент, когда всё пошло не так.
Шаг 3. Посмотреть контекст вокруг самого первого сбоя. Здесь и пригождается -B/-A: важно увидеть, что происходило до первой ошибки — что могло её спровоцировать:
grep -B 5 -A 10 'CRITICAL' service.log | head -30
Шаг 4. Проверить, не разошлось ли это по всему коду. Если в логе мелькает конкретное имя функции или модуля — например, PaymentGateway — стоит сразу поискать, где оно вызывается в самом коде, а не только в логе:
grep -rn 'PaymentGateway' --include='*.py' --exclude-dir=tests src/
Шаг 5. Зафиксировать находку и оставить след для команды. grep -c по типам ошибок за смену — быстрая сводка для тикета инцидента, без единой ошибки, введённой глазами:
grep -oE 'ERROR|WARN|CRITICAL' service.log | sort | uniq -c
Здесь -o выводит только совпавшее слово (без остального текста строки) на каждой своей строке, а связка sort | uniq -c — приём из главы про конвейеры — группирует и считает повторы. Результат — три числа: сколько ERROR, сколько WARN, сколько CRITICAL, без единого лишнего клика по файлу.
Это и есть та самая суперспособность из начала главы: пять команд, каждая — одна-две секунды на выполнение, и вместо блуждания по стотысячестрочному логу — конкретный ответ.
На практике почти никогда не пишут идеальный шаблон с первого раза. Обычная последовательность — сначала грубый grep -c, чтобы понять масштаб, потом сузить -n/-B/-A, чтобы найти нужное место, и только в конце — точный шаблон под конкретную задачу. Регулярка — не то, что придумывается заранее целиком, а то, что уточняется по ходу разведки.
Практика: grep-квест в терминале
Применим всё в учебном терминале. Честное предупреждение: grep здесь упрощённый — он ищет подстроку и не разбирает регулярные выражения, так что операторы из этой страницы отрабатывай потом в настоящем терминале. Зато флаги команд, конвейер и сама логика «найди иголку в стоге» — как настоящие.
В файле ~/logs/app.log среди сотни строк спрятана одна со словом ACCESS — в ней написан секретный код. Найди её c помощью grep (или cat app.log | grep ...), прочитай код и создай в домашней папке пустой файл, названный этим кодом: если код apple, то touch found_apple.txt.
- ○ found_cherry.txt
Как это устроено под капотом
И закрепи руками написание настоящих шаблонов — с регистром, кавычками и косыми всё должно быть точно до символа:
Цель: точность ≥ 90%
grep -in error app.log
Шпаргалка
| Запись | Что означает | Пример | Найдёт |
|---|---|---|---|
abc | буквально этот текст | grep error log | строки со словом error |
. | один любой символ | v..sion | version, но не vision |
\. | буквально точка | 1\.5 | 1.5, но не 105 |
[abc], [0-9] | один символ из набора/диапазона | v[0-9] | v1, v2... |
[^0-9] | один символ НЕ из набора | id[^0-9] | idx, но не id7 |
^, $ | начало / конец строки | ^From, done$ | позиция, не символ |
^$ | пустая строка | grep -c '^$' f | посчитать пустые |
* | предыдущий 0+ раз | ab*c | ac, abc, abbc |
+ (с -E) | предыдущий 1+ раз | ab+c | abc, но не ac |
? (с -E) | предыдущий 0–1 раз | colou?r | color и colour |
{n}, {n,m} (с -E) | ровно n / от n до m раз | [0-9]{4} | четыре цифры подряд |
-i / -n / -v | без регистра / с номерами / инверсия | grep -inv | — |
-c / -w / -r | посчитать / целые слова / рекурсивно | grep -rw log . | — |
-E | расширенный диалект ERE | grep -E '[0-9]{3}' codes.txt | включай для + ? |
\b | граница слова | grep 'log\b' | log, catalog, но не login |
(...), \1 (с -E) | группа-захват и обратная ссылка | grep -E '(\w+) \1' | the the |
[[:digit:]], [[:alpha:]] | POSIX-классы | grep '[[:digit:]]\{3\}' | три цифры, локале-независимо |
-A N / -B N / -C N | N строк после / до / вокруг | grep -C 2 panic log | совпадение с контекстом |
-l / -L | только имена файлов с / без совпадений | grep -rl TODO src/ | список файлов |
--include / --exclude | фильтр по маске имени файла (с -r) | grep -r --include='*.js' | только нужные расширения |
--exclude-dir | не заходить в каталог (с -r) | grep -r --exclude-dir=node_modules | обход без мусора |
-q | без вывода, только код возврата | grep -q ERROR log && ... | 0 — нашлось, 1 — нет |
-f file | шаблоны из отдельного файла | grep -f patterns.txt log | совпадение с любым из списка |
-H / -h | принудительно показать / скрыть имя файла | grep -h TODO *.js | без префикса имени |
-e | ещё один шаблон в той же команде (ИЛИ) | grep -e ERROR -e WARN | без переключения на -E |
-F | без regex вообще, буквальный текст | grep -F 'calc(x, y)' | никакие символы не операторы |
\d, \w, \s (с -P) | PCRE-сокращения: цифра / слово / пробел | grep -oP '\d{3}' | как в Python/JS |
Углубиться
Третий диалект: PCRE. Кроме BRE и ERE, у GNU grep есть флаг -P — Perl-совместимые регулярные выражения, самый богатый диалект: сокращения вроде \d (цифра) и \w (символ слова), ленивые квантификаторы, просмотры вперёд-назад. Именно PCRE-подобный синтаксис используют регулярки в Python, JavaScript и большинстве языков. В скриптах, которые должны работать на любой системе, на -P полагаться нельзя — он есть не во всех сборках grep, — но на своей машине это самый удобный режим.
POSIX-классы. Внутри квадратных скобок работают именованные классы: [[:digit:]] — то же, что [0-9], [[:alpha:]] — буквы, [[:space:]] — пробельные символы. Выглядят громоздко, но корректно работают с любыми локалями и алфавитами — в отличие от диапазона [a-z], который про кириллицу ничего не знает.
egrep и fgrep. В старых скриптах встречаются команды egrep (то же, что grep -E) и fgrep (то же, что grep -F — искать буквально, вообще без регулярных выражений). Они объявлены устаревшими: POSIX рекомендует формы с флагами, и новые скрипты стоит писать именно так.
-o — показать только совпадение. Обычно grep печатает строку целиком. Флаг -o печатает только совпавший кусок, каждое совпадение на своей строке: grep -oE '[0-9]+' log.txt вытащит из файла все числа. В связке с wc -l — готовый счётчик совпадений (не строк, как -c, а именно совпадений).
grep против ripgrep. На современных машинах (в том числе на многих чемпионатных стендах) рядом с grep часто стоит rg (ripgrep) — написанный на Rust инструмент с той же идеей, но заметно быстрее на больших деревьях каталогов и с более удобными настройками по умолчанию: rg сам игнорирует .git, уважает .gitignore и по умолчанию рекурсивен, то есть rg TODO уже эквивалентно grep -r TODO ., только без флага. Синтаксис шаблонов у него — PCRE-подобный, ближе к тому, что использует -P у grep. Знать grep всё равно обязательно: он есть на любой системе без установки, входит во все стандарты POSIX и остаётся тем, что спросят на экзамене и что точно найдётся на чужом сервере, куда rg никто не поставил.
Отличие POSIX-классов от диапазонов на практике. [a-z] работает только в локалях, где алфавит устроен как непрерывный диапазон байтовых кодов a-z — это верно для английского, но не для многих других языков и не для смешанных данных. [[:alpha:]] спрашивает у текущей локали «что тут вообще считается буквой» и работает предсказуемо в любой из них — цена этому предсказуемости — более громоздкая запись.
Дальше по теме:
- GNU Grep Manual — Regular Expressions — официальное руководство: точное описание BRE, ERE и PCRE со всеми операторами
- man 7 regex — справка по регулярным выражениям POSIX, общая для grep, sed и других утилит
Сводная проверка по всей главе: 14 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Искать текст в файле и в выводе другой команды, применяя флаги
-i,-n,-v,-c,-w,-rпо назначению. - Объяснять, почему шаблон берут в одинарные кавычки и что случится без них.
- Читать и писать шаблоны с точкой, экранированием, классами
[...]и инвертированными классами[^...]. - Прибивать шаблон якорями
^и$— в том числе находить пустые строки и строки-комментарии. - Пользоваться квантификаторами
*,+,?,{n,m}и знать, когда нужен флаг-E. - Отличать диалекты BRE и ERE и объяснять, почему
grep '{4}'иgrep -E '{4}'ведут себя по-разному. - Понимать жадность
*/+и уметь укрощать её классом-исключением[^...]или, если доступен-P, ленивым квантификатором. - Использовать группы захвата и обратные ссылки
\1для поиска повторов, а границу слова\b— для точечного ограничения совпадения. - Показывать контекст вокруг совпадения флагами
-A/-B/-Cи работать с деревом проекта через-l,--include,--exclude,--exclude-dir. - Собирать рабочие связки:
grep -r+find/xargsдля массовых операций, грубый шаблон для поиска утекших секретов перед коммитом. - Пользоваться кодом возврата (
grep -qв условиях) и флагами для скриптов:-f,-H/-h,-a. - Искать по нескольким шаблонам сразу через
-eи знать, когда достаточно-Fвместо полноценного regex. - Понимать, чем PCRE (
-P,\d,\w, просмотры вперёд) отличается от POSIX-диалектов и когда на него можно полагаться.
Куда дальше: проверенные ресурсы
- GNU Grep Manual — первоисточник: все флаги и все три диалекта регулярных выражений в одном документе.
- man grep (Ubuntu) — официальная man-страница: краткая версия того же, всегда под рукой по команде
man grep. - gitleaks — если понравилась идея грубого поиска секретов из раздела про живые сценарии: это тот же принцип, но с готовой библиотекой шаблонов под конкретные форматы ключей (AWS, GitHub, Slack и десятки других) и встраиванием в pre-commit хук.
- Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Имя grep — буквально команда из редактора ed 1970-х: g/re/p, «глобально найти по regular expression и напечатать». Кен Томпсон вынес её в отдельную программу — и она пережила сам редактор на полвека.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.