Писать парсеры вручную — то ещё удовольствие. Сначала для шейдерного фронтенда bgfx использовался Lemon parser generator. Код работал, но был нечитаемым, а при каждой смене грамматики приходилось заново вникать в результат. В итоге от Lemon отказались. С другой стороны — ad-hoc парсеры, написанные руками: циклы по символам, вызовы strchr/strncmp, счётчики строк и вечный страх пропустить крайний случай. Такие сканеры быстрые и без зависимостей, но повторяют одни и те же грабли: пропуск пробелов, разбор идентификаторов, обработку \r\n и \n, off-by-one на кривом вводе. Каждый новый парсер тащил за собой собственный зоопарк багов.
Поэтому появился bx::Scanner — не парсер-генератор и не библиотека для PEG, а небольшой набор переиспользуемых примитивов. Zero-copy, без аллокаций, читается в дебаггере. Сканер не владеет текстом и никогда его не копирует: любой результат — это StringView, указывающий прямо в исходный вход. Даже пустой результат указывает на текущую позицию, а не в никуда. Курсор один, сам по себе не двигается: accept, acceptWhile и acceptUntil двигают его вперёд, peek только проверяет, seek и reset позволяют прыгать, но автоматического бэктрекинга нет. Встроен подсчёт строк и колонок: переход через новую строку обновляет номер строки, обе величины начинаются с единицы, так что сообщения об ошибках почти бесплатны. Вместо мини-языка — классы символов: Class::Space, Class::NonSpace, Class::Identifier, Class::EndOfLine, Class::NewLine. Что-то специфичное — просто обычный предикат bool(*)(char). Весь публичный интерфейс помещается в один маленький заголовок.
Работа со сканером одинаковая: создаёшь Scanner над StringView, потом peek и accept, пока не закончится вход. accept возвращает StringView: если токен не совпал, возвращается пустая строка, а курсор остаётся на месте. У StringView нет operator bool, поэтому успешный матч выглядит как !scanner.accept('=').isEmpty().
Самый простой помощник — LineReader. Он режет вход на строки, понимает \n и \r\n, срезает лишний \r. Цикл нужно проверять через isDone(), а не через !next().isEmpty(): пустая строка — это валидный результат, а не конец файла.
INI-парсер на bx::Scanner заменил стороннюю библиотеку. Ключевой приём — под-сканер на одну строку: acceptUntil(Class::EndOfLine) возвращает StringView, из которого делается отдельный Scanner. Внутренний сканер физически не может выйти за конец строки, даже если строка битая. В ad-hoc версии такая ошибка — классика: парсер ищет закрывающую ] или новую строку и улетает в следующие строки.
При разборе URL пустой токен — это норма. У http://example.com нет пути, у большинства URL нет userinfo. acceptUntil("://") вернёт пустую строку и для /tmp/file, и для ://host, поэтому вопрос «была ли схема» задаётся отдельно через accept("://"). Правило простое: возвращаемое значение — это токен, а структурный вопрос «присутствовал ли разделитель» решается через peek или accept. Целый парсер URL занял 75 строк, а не 23 тысячи строк C++.
Нормализация путей работает с буквами дисков, разделителями / и \, схлопыванием . и ... Задача сканера — отдавать компоненты по одному; обработка .. отматывает выходной буфер, а не вход. В разборе вывода atos или addr2line LineReader и Scanner заменили клубок из strstr и арифметики указателей. Благодаря этому удалось выкинуть зависимость от INI-библиотеки и три рукописных копии одного и того же цикла по пробелам и идентификаторам. Парсеры не обязаны быть сложными.