Мы никогда не читаем код как книгу — мы выбираем только конкретные интересующие места. Такие места обычно запоминаются ассоциативно, например по имени функции, строковому литералу, импорту библиотеки, комментарию и т. д. Перейти от ассоциации к файлу, а тем более к конкретной строчке кода не всегда легко. Особенно если оперируешь большим количеством проектов с активно меняющейся кодовой базой. В таких случаях выручает удобный инструмент текстового поиска.
Эффективность такого инструмента определяется как скоростью работы, так и удобством использования. В частности, кастомизация под себя позволяет разгрузить
Дисклеймер
-
Решения по типу SourceGraph в статье не рассматриваются намеренно, речь идет именно об удобстве локального поиска.
-
Полнофункциональные терминальные решения на базе vim/neovim не рассматриваются, так как имеют довольно высокий порог входа.
-
Я ни в коем случае не эксперт по bash, поэтому очень приветствую замечания и улучшения по скриптам.
Введение об идеальном поисковике
Прежде чем поделиться своим рецептом, я бы хотел кратко пояснить читателям истоки именно такого видения идеального поисковика. То решение, к которому мы придем в конце статьи, во многом навеяно предыдущим опытом и сформировавшимися привычками. Постараюсь упаковать и то, и другое в пару абзацев! Если же хочется опустить лирику и сразу посмотреть, что получилось в итоге, можете перейти в раздел «Собираем все вместе».
Я начинал свою карьеру как разработчик под Windows в поздних нулевых. По моему субъективному мнению, с инструментами разработки тогда все было хорошо хотя бы по причине Visual Studio 2005. Разработку под Windows эта IDE позволяла делать с высоким уровнем комфорта, все возможности были доступны из GUI, и практически никогда не требовалось оперировать терминалом. Еще на Windows был и есть Total Commander. Это буквально первая программа, которая предстала передо мной на первом персональном (в те времена, скорее, «семейном») компьютере. Азы работы с файлами и каталогами учил в нем, с тех пор всегда ставлю TC одним из первых на свежую инсталляцию ОС от Microsoft. Total Commander — действительно удобный, проработанный и расширяемый файловый менеджер, полных аналогов которому на других платформах пока мне найти не удалось.
К чему я? Одна из киллер-фич TC, по моему мнению, — поиск файлов по имени и содержимому. Я как-то уже упоминал о нем в комментарии — обсуждение и сама статья про Far заслуживают внимания. Основной сценарий использования поиска такой:
-
Открыть папку с большой кодовой базой или сразу со всеми проектами.
-
Ввести маску для файлов, например `*.cpp;*.h;*.c`.
-
Ввести искомую подстроку, например
malloc
. -
Ознакомиться со списком найденных файлов.
-
Посмотреть результаты точечно с помощью встроенного просмотрщика (Lister).
Ключевое удобство в том, что можно нажать F3 (хоткей Lister) прямо на строчке в диалоге поиска (не выводя результаты в панель!), чтобы просмотреть содержимое найденного файла. При этом Lister понимает, что его вызвали в контексте поиска, и повторное нажатие на F3 в нем скроллит к искомой строке (в примере это malloc
). Последующие нажатия скроллят к следующему вхождению строки в файле, если таковые имеются. Такая получилась нативная интеграция поисковика с просмотрщиком. По нажатию на F4 открывается редактор, который, кстати, можно выбрать на свой вкус.
В итоге выработалась привычка использовать IDE для проекта, над которым в моменте активно работаю, и Total Commander для широкого поиска по остальной кодовой базе и разнообразным конфигам. Приходилось работать с самыми разными кодовыми базами, порой очень дремучими и запутанными — задачи выполнялись, тулинг не подводил. Эта связка отлично себя показывает на практике при работе в Windows и по сей день.
Но где-то же должна быть завязка статьи? :) Она заключается вот в чем: за последние пару лет я окончательно перешел на MacOS в качестве рабочей ОС. Большую часть времени за работой провожу в ней. И поначалу мне очень недоставало хорошего инструмента для поиска — удобство предыдущего подхода просто нечем было восполнить. Поэтому, когда я наконец-то набрел на «конструктор», из которого можно собрать что-то на свой вкус, именно привычный вариант работы, как в TC, я взял за образец.
Поиск лучшего решения
Итак, на старте мы имеем MacOS + желание быстро и удобно искать текст. Давайте прикинем, что с этим можно сделать.
Что не сработало
Идея собрать что-то под себя пришла не сразу, поэтому я начал с перебора готовых коробочных решений. Каждый из инструментов ниже, помимо всего прочего, позволяет искать файлы по названию и по содержимому. Не хочется утверждать, что это совсем нерабочие варианты, но лично у меня «магии на кончиках пальцев» не случилось.
Midnight Commander. Старичок, ветеран индустрии. Думаю, что если бы когда-то привык к нему, а не к Total Commander, то функциональность вполне устроила бы. Внутри есть очень похожий на TC поисковик (а может, это, наоборот, TC похож на MC, who knows) с похожими хоткеями и UX. Однако встроенный просмотрщик и редактор весьма слабые, а настройка под себя показалась замороченной.
Commander One. Один из лучших клонов Total Commander на MacOS. В целом пользоваться можно, и даже местами удобно. Но поиск в нем явно не самая сильная сторона: и встроенный Lister проигрывает в функциональности, и хоткеи работают не так. Пользоваться можно, но снова не то.
IDE. Все хорошо, но медленно. Обычно в IDE открывается один проект или группа репозиториев внутри одного проекта. Даже в относительно легковесной VS Code интерфейс становится перегруженным, а команды начинают работать медленно. Плюс открывать IDE под каждую необходимость поискать что-то не очень оптимально, хочется решение пошустрее.
Классические CLI-утилиты из коробки. При необходимости искать с помощью grep и find, конечно, можно, но лично для меня это никогда не было быстрым и удобным способом что-то найти. Из минусов можно отметить высокие накладные расходы (банально нужно много печатать) и низкую интерактивность. Из плюсов — наличие в любом дистрибутиве.
В итоге вопрос подвис без какого-либо решения. Возможно, и к лучшему, что не стал целенаправленно искать серебряную пулю, ибо за время, пока проблема настоялась, я набрел на несколько весьма интересных инструментов.
Что будем использовать
В последнее время появилось довольно много современных реинкарнаций классических линуксовых утилит. Многие из них пишутся на Rust/Go, что дает кросс-платформенность и многопоточность из коробки. Новые языки привлекают большие сообщества, у проектов много контрибьюторов и тысячи звезд на гитхабе. Отсутствие необходимости соблюдать обратную совместимость развязывает руки в плане оптимизации и построения более функциональных консольных UI. Некоторые из таких утилит получили заслуженное признание в комьюнити, взглянем повнимательнее на несколько из них.
bat. Утилита bat — это современная альтернатива классической команде cat
в Linux, предназначенная для вывода содержимого файлов в консоль. Одним из ключевых преимуществ bat
является подсветка синтаксиса, которая делает чтение кода и конфигов куда более удобным и наглядным. Кроме того, bat
поддерживает нумерацию строк, что упрощает навигацию по файлу и анализ его содержимого. Утилита также интегрируется Git, показывая изменения в файлах. bat
имеет встроенную поддержку для просмотра содержимого нескольких файлов одновременно, а также возможность отображения содержимого в виде страниц. То есть тот же cat
, только функциональнее. На настоящий момент это мой избранный легковесный просмотрщик.
fzf. Утилита fzf — это интерактивный инструмент командной строки для поиска и фильтрации текста. Он принимает на вход список (обычные строки, разделенные символом переноса) и показывает окошко с возможностью нечеткого поиска в этом списке. Так можно быстро находить файлы, команды в истории, процессы и др. Важно заметить, что fzf
не просто хорошо решает свою задачу, а еще и гибко конфигурируется, что позволяет использовать его в комбинации с другими утилитами.
rga. Утилита rga
(ripgrep all) — это расширение утилиты ripgrep
, предназначенное для поиска текста в различных типах файлов, включая архивы и документы. В отличие от стандартного ripgrep
, rga
поддерживает поиск внутри PDF, DOCX, ZIP и т. д. в зависимости от установленных плагинов. Он использует специализированные конверторы, например pandoc
, для извлечения текста из «сложных» форматов файлов. rga
сохраняет высокую скорость работы благодаря эффективному алгоритму поиска (в т. ч. по регулярным выражениям), характерному для ripgrep
.
Собираем все вместе
Впервые способ заставить работать fzf
с rga
попался мне на глаза в заметке:
Концептуально суть заключается в следующем:
-
fzf
отвечает за отображение легковесного UI (список найденных файлов/строк и превью результатов), а также кастомизацию хоткеев и обработку ввода; -
rga
выступает в роли поисковика файлов по содержимому, делает "heavy lifting"; -
bat
опционально используется вместо встроенного превью; -
скрипт в виде функции кладется в
.bashrc`/`.zshrc
, а затем вызывается из терминала по имени.
Не забудьте установить утилиты и перезапустить оболочку после правок.
Вариантов таких сборок нашлось немало, например:
-
https://github.com/phiresky/ripgrep-all/wiki/fzf-Integration
-
https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interactive-ripgrep-launcher
-
https://stackoverflow.com/questions/77415675/how-to-use-rga-ripgrep-all-with-fzf-for-searching-the-pdf-file-and-then-using. Один из них даже встроили в
rga
в виде отдельного бинарника. -
https://github.com/phiresky/ripgrep-all/blob/master/src/bin/rga-fzf.rs. С одной стороны работает из коробки, с другой — почти не кастомизируется.
Идея везде примерно одинаковая, скрипты различаются способом показа результатов поиска (только имена файлов или имена с номерами строк), формированием превью и биндов хоткеев. Изучив несколько вариантов и изрядно наигравшись с комбинацией параметров, я пришел вот к такой версии (репозиторий):
qse() {
RG_PREFIX="rg --files-with-matches"
local file
file="$(
FZF_DEFAULT_COMMAND="$RG_PREFIX '$1'"
fzf
--preview="[[ ! -z {} ]] && rg --pretty --context 5 {q} {}"
--disabled --query "$1"
--bind "change:reload:sleep 0.1; $RG_PREFIX {q}"
--bind "f3:execute(bat --paging=always --pager=less --color=always {} < /dev/tty > /dev/tty)"
--bind "f4:execute(code {})"
--preview-window="70%:wrap"
)" &&
echo "$file"
}
Разберем ключевые моменты:
-
В
RG_PREFIX
указывается базовая команда поиска с флагами по умолчанию; я остановился наrg
, потому что поиск по отличным от кода файлам мне требуется гораздо реже. -
В
FZF_DEFAULT_COMMAND
указывается, чем fzf ищет по умолчанию (вместоfind
). -
--preview
определяет команду, которая отобразит найденный результат; она будет вызываться каждый раз при навигации по результатам поиска. -
--disabled
отключает поиск в fzf, превращая утилиту в UI-прослойку. -
--bind
на событиеchange
перезапускает поиск при вводе. -
--bind
на клавиши F3 и F4 запускаетbat
и VS Code соответственно. -
С помощью
$file
выбранный файл выводится в консоль.
Как нетрудно заметить, здесь фактически воссоздан функционал поиска из TC. Работает быстро, плавно и отзывчиво благодаря перезапуску поиска после каждого нажатия (событие change). Есть подсветка найденных результатов и возможность запустить просмотрщик/редактор.
Давайте посмотрим, как работает вживую, на небольшой демонстрации. Специально для нее я подготовил каталог, в который склонировал код Kubernetes, VictoriaMetrics и fzf. Сначала выполняется поиск всех объявлений функций в Go по ключевому слову func
, затем навигация по результатам, точечный просмотр файлов и повторный поиск с более специфичным запросом func TestQueue
. Выглядит вот так:
Почему qse? Хотелось придумать короткий и удобный шорткат, изначально это был qs (quick search, qtros search, whatever). Далее в процессе отладки я наплодил несколько версий в алфавитном порядке: qsa, qsb, ..., qse. Дойдя в экспериментах до версии «e», я получил все желаемое поведение и уже успел немного привыкнуть к имени. Поэтому qse.
За время написания статьи я еще немного покрутил скрипт: заменил превью на более функциональное на основе batgrep
. Эту и последующие правки можно будет найти в репозитории. Вот как это выглядит:
Кстати, пока тестировал свою сборку, обнаружил баг в fzf, связанный с кэшированием результатов. Его уже успели поправить, 27 октября случился релиз 0.56.0. Поэтому при использовании последней версии у вас не должно быть проблем!
Заключение
В статье рассмотрен способ создания специализированного поисковика из подручного опенсорса. Несмотря на простоту и небольшой размер, получившийся скрипт уже вовсю используется в работе, помогая искать диагонально по десяткам репозиториев крупных проектов. Примечательно, что каждый может без особого труда докрутить скрипт под себя: выбрать альтернативный просмотрщик, поставить другие хоткеи, искать не только по файлам и т. д. Надеюсь, что опыт из статьи оказался полезным и что вы нашли в ней что-то новое!
Автор: QtRoS