Поиск строки в дереве из десятков тысяч файлов - проверка на терпение и для человека, и для диска. Команда grep -rn без разбора читает всё, что попадётся на пути: каталог зависимостей, собранные артефакты, минифицированные скрипты, где одна строка тянется на несколько экранов. Утилита ripgrep с коротким именем rg подходит к той же задаче иначе: она заранее отбрасывает файлы, которые рабочему дереву не нужны, раскидывает обход и чтение по ядрам процессора и держит под рукой быстрый движок регулярных выражений с юникодом. Ниже - механика обоих инструментов, честный разбор мест, где классика незаменима, и настройка быстрого поиска в vim и VS Code, чтобы терминал и редакторы работали с кодом одинаково.
Умные умолчания ripgrep и каскад правил игнорирования файлов
Главная причина скорости rg не в количестве потоков, а в списке файлов, которые инструмент вообще не открывает. Запуск из корня проекта уже даёт осмысленный результат, потому что фильтрация срабатывает до чтения первого байта. Умолчания складываются из четырёх слоёв:
- правила хранилища версий из файлов .gitignore применяются автоматически, вместе с глобальным списком исключений и правилами из родительских каталогов;
- файл .ignore добавляет исключения поверх хранилищных, когда игнорировать нужно только для поиска по коду, не трогая настройки хранилища;
- файл .rgignore наслаивается ещё выше и закрывает личные потребности вроде локального мусора сборки;
- скрытые файлы и каталоги с точкой в начале имени пропускаются, а двоичные распознаются по нулевому байту в начале чтения и отбрасываются до запуска выражения.
При конфликте побеждает более личный слой: .rgignore сильнее .ignore, тот сильнее правил хранилища. Никакой тайны, обычный каскад с понятным приоритетом. Шаблоны одного файла проверяются одновременно, поэтому дюжина правил обходу не мешает.
Когда фильтры мешают делу, они снимаются разом или по отдельности:
rg -uuu "Listen 80" /srv/app
rg --hidden --no-ignore "Listen 80" /srv/app
rg -a "шаблон" dump.bin
Первый ключ -uuu снимает фильтрацию по ступеням: сначала вернутся игнорируемые файлы, затем скрытые, затем двоичные. Точечные ключи гибче: --hidden возвращает скрытые имена, --no-ignore выключает только правила игнорирования, а -a читает двоичные файлы как текст. В системных каталогах вроде /etc без --hidden легко потерять часть конфигурации, ведь точка в начале имени там не редкость.
Перед серьёзным поиском полезно оценить объём будущей выборки:
rg --files /srv/app | wc -l
Так видно, сколько файлов вообще попадёт под поиск. Разница между "все файлы дерева" и "файлы после фильтров" на реальном проекте бывает кратной, и именно она, а не только параллелизм, определяет время отклика.
Постоянные ключи выносятся в файл конфигурации: переменная окружения RIPGREP_CONFIG_PATH задаёт путь, каждая строка файла становится отдельным аргументом, строки с решёткой - комментариями, ключ --no-config отключает файл для разового запуска.
Многопоточный обход и движок выражений без обратного хода
Второй источник скорости - параллелизм. Обход каталогов построен на параллельном итераторе без общих блокировок: потоки разбирают поддеревья без ожидания в очередях. Поиск внутри файлов распределён по потокам, их число по умолчанию соответствует количеству ядер, а ключ -j ограничивает его вручную, когда нагрузка на сервер и без того высока.
Третий источник - движок регулярных выражений. В его основе конечные автоматы: время поиска растёт линейно по длине входа, и выражение вроде (a+)+, способное затянуть поиск с откатом назад на часы, отрабатывает за предсказуемое время. До запуска автомата движок выделяет из шаблона литеральные куски и ищет их быстрыми алгоритмами, а на процессорах с поддержкой SIMD подключаются векторные инструкции.
Юникод встроен в сам автомат, декодирование utf-8 происходит прямо внутри него, поэтому поиск без различия регистра по кириллице не зависит от системной локали. У классического grep тут уязвимость: в локали вроде C он работает побайтово, и grep -i не найдёт слово, набранное в другом регистре, если текст кириллический. В ripgrep кодировка utf-16 с меткой порядка байтов распознаётся автоматически, а ключ -E задаёт произвольную кодировку для особых случаев.
Честности ради: на одиночном файле с простым шаблоном разница невелика, у GNU grep есть свои оптимизации для буквенных строк, и он по-своему быстр. Разрыв растёт вместе с деревом и рождается из трёх источников сразу: короткий список файлов, параллельные ядра, надёжный движок. Чтение устроено двумя способами - отображением файла в память или потоками через буфер, и инструмент выбирает способ сам.
Для сложных выражений у быстрого движка есть ограничение: без отката он не умеет заглядывание назад и обратные ссылки. Их даёт ключ --pcre2, подключающий движок PCRE2 в сборках с его поддержкой. У классического подхода зеркальная особенность: GNU grep с ключом -P использует PCRE со всеми его капризами, включая опасный откат на неудачном шаблоне.
Честные сильные стороны grep в конвейерах и на минимальных системах
Быстрый инструмент не отменяет классику. Grep входит в набор утилит POSIX и стоит практически везде: минимальный образ контейнера, режим восстановления системы, машина, куда нельзя ничего доустановить. Скрипт с grep выполнится на любой unix-подобной системе, скрипт с rg - только там, где утилита уже есть. Установка в большинстве дистрибутивов сводится к пакету ripgrep:
apt install ripgrep
dnf install ripgrep
Фильтрация потоков - вторая опора классики:
journalctl -u nginx | grep -i error
Ripgrep без аргумента пути сам читает стандартный ввод, поэтому конвейер с ним выглядит так же:
journalctl -u nginx | rg -i error
Для рекурсивного поиска классика требует явных исключений, иначе прочитает и каталог .git, и каталог зависимостей:
grep -rn "session_start" /srv/app --exclude-dir={.git,node_modules} --include="*.py"
Фигурные скобки раскрываются оболочкой bash в список каталогов. Каждый ключ приходится помнить и писать руками, у rg те же правила применяются молча. Буквы ключей при этом совпадают: -n, -i, -w, -l, -c, -o, -A, -B, -C значат одно и то же в обоих инструментах, поэтому переучивание сводится к двум привычкам: рекурсия включена по умолчанию, фильтры писать не нужно.
Вывод по выбору прост. Одиночный файл, короткий конвейер, скрипт для чужих машин - grep. Большое рабочее дерево, десятки запросов в день, интеграция с редактором - rg.
Типы файлов, шаблоны имён и умный регистр в повседневных запросах
Поиск по коду редко нужен "по всему подряд". Ключ -t сужает выборку до файлов выбранного языка:
rg -t py "def main" /srv/app
rg -t js "addEventListener" /srv/www
rg -T js "version" /srv/www
Список встроенных типов показывает --type-list, свои типы собираются на лету ключом --type-add и выручают на конфигурациях без отдельного типа:
rg --type-add 'conf:*.{conf,ini,yaml}' -tconf "listen" /etc
Шаблоны имён решают остальное. Ключ -g принимает правила в духе правил игнорирования, повторяется сколько нужно, восклицательный знак переворачивает смысл:
rg -g '!*.min.js' -g '!dist/**' "render" /srv/www
Регистр ускоряет или точнит поиск. Ключ -S включает умный регистр: шаблон из одних строчных букв ищется без различия регистра, стоит вписать заглавную - и поиск начнёт различать регистр сам. Ключ -w ограничивает совпадение границами слова, -e принимает несколько шаблонов сразу, а -C показывает контекст вокруг находки, число строк задаётся после ключа. От минифицированных файлов спасает пара ключей:
rg --max-columns=150 --max-columns-preview "token" /srv/www
Длинные строки обрежутся, на месте хвоста появится пометка. Коды возврата совпадают с классическими: ноль - совпадения есть, единица - совпадений нет, двойка - ошибка, поэтому rg встаёт в скрипты вместо grep без переписывания логики.
Практические сценарии от большой выборки до сжатых логов
Типовые задачи на сервере закрываются готовыми строками. Поиск по большому дереву с умным регистром:
rg -n -S "SESSION_TIMEOUT" /srv/app
Выборка имён по содержимому - второй частый случай, когда нужны файлы, а не строки:
rg -l --null "password" /etc | xargs -0 -r wc -l
Ключ -l печатает только имена, --null отделяет их нулевым байтом, и список безопасно уходит в xargs даже с пробелами в путях. Подсчёт по файлам выручает при поиске самых шумных журналов:
rg -c "ERROR" /var/log -g '*.log' | sort -t: -k2,2 -rn | head
Сжатые журналы часто обходят стороной, и напрасно: ключ -z понимает распространённые форматы сжатия, включая gzip, bzip2, xz и zstandard:
rg -z " 500 " /var/log/nginx -g '*.gz'
Несколько шаблонов сразу и просмотр будущей замены без правки файлов:
rg -e "critical" -e "warn" /var/log/app/app.log
rg -r "192.168.10.20" "192.168.0.10" -n /etc
Вторая строка печатает совпадения с уже подставленным новым значением, файлы остаются нетронутыми, а для отчётов и скриптов есть формат --json. Связка с нечётким поиском закрывает последнюю рутину: конвейер rg --files | fzf отдаёт фильтру чистый список имён, и переход к нужному файлу занимает пару букв. Обычный обход для фильтра тянет весь мусор дерева, фильтрованный список короче и приходит быстрее.
Внешняя команда поиска в vim и список quickfix для быстрых переходов
Vim зовёт поиск через внешнюю команду, и это ключ к скорости. За команду отвечает параметр grepprg, по умолчанию редактор вызывает системный grep с ключом -n. Две строки в .vimrc переключают его на быстрый инструмент:
set grepprg=rg\ --vimgrep
set grepformat=%f:%l:%c:%m
Пробел в значении экранируется обратным слешем, иначе параметр разорвётся на части. Ключ --vimgrep печатает каждую находку строкой вида путь:строка:столбец:текст, а grepformat объясняет vim, как разбирать такой вывод. Дальше работает штатная механика:
:grep "SESSION" /srv/app
:grep! "def main" %
:copen
:cnext
Находки попадают в список quickfix, :copen показывает их окном, :cnext и :cprev переносят курсор между ними, восклицательный знак прыгает к первому совпадению сразу, а % подставляет имя текущего файла для локального поиска. Запрос идёт от текущего каталога vim, поэтому перед серией поисков стоит выполнить :cd в корень проекта. Для локальных списков есть пара :lgrep и :lopen.
Встроенная команда :vimgrep читает файлы силами самого редактора, и на большом дереве она заметно медленнее: внешнему rg vim лишь передаёт шаблон и разбирает готовый ответ. Расширения подвязывают и нечёткий выбор: конвейер rg --files с фильтром получает горячую клавишу, и прыжок к файлу по двум-трём буквам становится привычкой.
Поиск в VS Code поверх движка ripgrep и его регулировка
В редакторе VS Code поиск по всем файлам проекта работает поверх того же движка: редактор вызывает rg под собственной оболочкой, разбирает вывод и рисует результаты группами по файлам. Для практика это значит, что терминал и редактор ищут одинаково: правила игнорирования хранилища по умолчанию учитываются, регулярные выражения совпадают, привычки из консоли переносятся в интерфейс без потерь.
Параметры повторяют ключи командной строки. Параметр search.useIgnoreFiles отвечает за учёт правил игнорирования, search.exclude добавляет исключения в формате шаблонов, например каталог сборки, а поля включения и исключения в панели поиска - те же ключи -g в графическом виде. Параметр search.smartCase включает умный регистр, знакомый по ключу -S.
Практический выигрыш от общей механики двоякий. Исключения настраиваются один раз: то, что вычищено из поиска в редакторе, пропадает и из терминальных запросов, потому что правила лежат в общих файлах игнорирования. Работает и обратное направление: строка в .rgignore убирает каталог из обоих мест сразу. Панель редактора сильна предпросмотром замены и группировкой находок, терминал - скриптами и конвейерами, а движок один и ведёт себя предсказуемо в обеих средах.
Итоговая развилка проста. Grep остаётся универсальным языком поиска, который понимает любая unix-подобная система, а ripgrep берёт объёмом: большим деревом, десятками запросов в день, редакторской интеграцией. Начать можно с малого: пакет ripgrep из репозитория системы, две строки с grepprg в .vimrc, беглый взгляд в параметры поиска редактора. Умолчания с фильтрами, параллельные ядра и движок без обратного хода сделают остальное, и пауза между нажатием Enter и первым результатом сойдёт на нет.