--- title: Идеальный формат конфигов * date: 2026-03-14T17:21:59+03:00 tags: ["go", "conf"] category: - IT - Мои проекты --- \* лично для меня В общем, случилось и на неделе я таки присвоил тег v1 для своей самописной Go библиотеки для разбора конфигов! Но обо всём по порядку. Или можно пропустить предысторию и сразу [перейти к описанию библиотеки](#conf-v1). # Предыстория Около месяца назад я задумался написать небольшую утилиту для себя, которая бы организовывала для меня рабочее окружение. Не важно сейчас, как именно должна была организовывать, а важно, что эта утилита должна бы была иметь весьма разухабистый конфиг вследствие своей планируемой гибкости. И встал вопрос, а какой формат конфигов использовать? Казалось бы, возьми yaml, toml, на худой конец, json (hjson, json5, итп). Даже думал об ini формате! Но всё было не то... И дело даже не в моём NIH синдроме. А они все мне не подходят! ## YAML Отвратительный язык! Начиная с отступов пробелами, что я ненавижу,[^1] продолжая тем, что у него спека способна по объёму поконкурировать с спекой XML и заканчивая весельем с ошибками когда строки _внезапно_ парсятся как числа и всё ломается! ## TOML На самом деле, самый адекватный из вариантов, но его синтаксис... Ну скажем так, на любителя. Но да, всяко лучше YAML. Всё что угодно лучге YAML. Гори в аду, YAML! ## JSON Это вообще не язык для конфигов и не язык разметки. А формат серилизации объектов. Кому вообще первому пришло в голову в нём конфиги хранить? Его производные — это уже какой-то набор костылей. Зачем мучать стюардесу? ## INI Первый из подборки язык который именно изначально для описания конфигураций. Но он уж больно ограниченный, да и гнилостный душок микрософта... Короче, я не стал искать дальше оправданий и засучи́л рукава и решил написать идеальную (для себя) библиотеку конфигураций! За основу синтаксиса я взял формат конфигов у таких никсовых приложений, как NGINX, bind9 и прочих подобных. Во-первых, это красиво. Во-вторых, это привычно. Из других требований кроме привычности, была гибкость, которая выражается в возможности делать сколь угодно глубокую вложенность в конфигах. Но это всё было фоном, а главные мои требования были всё же нефункциональными: 1. Самое главное, мне должно _нравиться_. Я понимаю, что это никак не формализовать, это можно только почувствовать. Именно поэтому не подошли ни TOML, ни INI, ни в т.ч. YAML. Они мне _не нравятся_ внешне. 2. Не менее важно, что мне его должно хватать. Про вложенности я уже говорил. 3. И чтобы служило мне так десятилетиями без изменений. То есть, чтобы мне надо было эту библиотеку разработать один раз, а потом, в идеале, никак и никогда её не менять. Максимум подправлять под реалии новых версий языков, что-то такое. Я вообще люблю вещи из разряда «раз и навсегда». Может это старость? Немного подумав над синтаксисом я пришёл к тому, что мне нужна максимальная примитивность. Всего две формы записи: 1. Имя аргумент1 аргумент2 ... аргументN; 2. Имя аргумент1 аргумент2 ... аргументN { ...вложенные директивы... } По сути это и есть весь базовый синтаксис! Просто последовательность директив, каждая из которых просто обязана иметь имя. Причём неуникальное! Требование к уникальности имён — уже ограничение и неуниверсальность. Аргументы только самых базовых типов — строка (причём в разных кавычках в зависимости от контекста, например, \` для многострочных строк), числа как целочисленные, так и с плавающей точкой, булевы значения, и один особый тип: ident (то есть какая-то строка без кавычек, например, идентификатор или имя). Мне больше не надо! Даты? Строка! Промежутки времени? Тоже строка! Зачем отдельно-то? Примерно так я видел для себя идеальный формат конфигов. Да, очень сумбурно и неточно, но когда меня это останавливало? Решил накидать формальную грамматику, так как писать вручную парсер уж сильно не хотелось. Сначала написал её для [egg](https://gitlab.com/cznic/egg), немного помучался с API сгенерированного парсера, но потом всё же всё заработало! Кроме... Кроме того, что я наткнулся на неприятное свойство поведения: если парсер натыкается на неожиданный символ — он выдавал непонятную без полулитра ошибку вида "index of array out of range". Причём без номера строки и символа. Сиди и гадай, что пошло не так. Убив на это без малого пару дней, я так и не смог сделать так, чтобы ошибка была более человеческая (типа «строка 2 символ 4: ожидалось что-то, а тут что-то другое»). Поэтому я принял волевое решение переписать совсем с нуля. Взял другой [генератор парсеров](github.com/mna/pigeon), переписал грамматику c EBNF на PEG [^2] и ... И получилось **гораздо** более элегантно, чем с egg! Счастью моему не было предела, когда я получил первый успех! Да, конечно, потом пара дней полировки и обвешивания необязательными, но приятными фичами и готово! После того как я попробовал на практике свою библиотеку в одном простеньком проекте ([о нём в конце](#pose)) — я с чистой совестью присвоил библиотеке тег стабильной версии, т.к. я получил то, что хотел и больше править её в ближайшее время я не собираюсь. [^2]: https://git.neonxp.ru/conf/diff/parser/grammar.peg?id=00394a80501960ad26787b5c44435ed5ed67ad84 # conf v1 Встречайте: - [go.neonxp.ru/conf](https://go.neonxp.ru/conf) - [Гит репозиторий](https://git.neonxp.ru/conf) - [Документация на pkg.go.dev](https://pkg.go.dev/go.neonxp.ru/conf) Как я уже говорил, синтаксис очень простой. Для наглядности я приведу сразу пример, который покажет, по сути, все возможности. Да, возможностей не много, потому что я ценю минимализм, как уже говорил выше. ```nginx # Две одноименных директивы some directive; some other directive; string_val "value"; int_val 123; float_val 123.321; bool_val true; xdg_config_dir HOME ".config" # Если получать через StringExt("/", os.LookupEnv), то получится # $HOME + "/" + ".config" = "/home/ИМЯ_ПОЛЬЗОВАТЕЛЯ/.config" group1 "some" "args" "and" "body" { group2 123 321 { group3 true false true { key value; # One line comment! } } } ``` Из примера выше мы видим: - две директивы с одинаковым именем (`some`) - несколько директив с аргументами разных типов (`*_val`) - директивы с вложенными поддирективами (`group*`). Причём, наличие тела `{...}` у директивы не отменяет возможности передать и аргументы до тела. Единственное, тело должно быть одно и в конце директивы. Зато после него не нужно ставить `;`, парсер и так понимает что раз тело закончилось, то и директива закончилась. - отступы могут быть как табуляторами, так и пробелами. Но я прошу использовать именно табуляторы, потому что только табуляторы это правильно.[^1] Хер вы меня заставите передумать! И всё! Просто и очень наглядно. Идеально для конфигов! [^1]: https://neonxp.ru/posts/2025-04-05-tabs-or-spaces/ ## Использование в Go Естественно, не могло быть и речи о анмаршалинге этого формата на структуры, как это делается у JSON YAML и прочих. Но это и не надо! У библиотеки есть несколько встроенных типов, таких как: - [Group](https://pkg.go.dev/go.neonxp.ru/conf@v1.0.1/model#Group) - группа директив. В том числе и тело директивы. Всё просто! Из методов есть базовые методы для получения конкретных директив из группы и простой фильтр. - [Directive](https://pkg.go.dev/go.neonxp.ru/conf@v1.0.1/model#Directive) - для директив. У него есть несколько методов для типизированного получения первого из аргументов директивы (того что после имени директивы), также метод получения всех аргументов и тела. Так же есть и специальный метод StringExt[^3], который сливает все аргументы в одну строку с разделителем `sep` и пропуская аргументы типа `Ident` через переданную функцию `identLookup`. [^3]: https://pkg.go.dev/go.neonxp.ru/conf@v1.0.1/model#Directive.StringExt Это два самых главных типа. Помимо них есть ещё и Ident о котором я говорил выше и тип Lookup, который определяет функцию подстановки для метода StringExt, намеренно сделанный совместимым со стандартным [os.LookupEnv](https://pkg.go.dev/os#LookupEnv). Я постарался очень и очень поверхностно дать описание API, т.к. можно подробно прочитать об API и на [pkg.go.dev](https://pkg.go.dev/go.neonxp.ru/conf). А чего же не хватает у в этом API? Записи конфига! Да! Есть только Load и LoadFile[^4], но нет никакого Write, Marshal и чего-то такого! Я долго думал, как это сделать. Ведь всё таки если делать запись, то по хорошему надо следить за тем, чтобы и комментарии сохранялись, причём строго там, где они были в оригинальном конфиге. Более того, по хорошему, нужно сделать так, чтобы после LoadFile → WriteFile полученный файл должен побайтово совпадать с тем что было. Да, дохрена забот! А потом я подумал «А зачем мне вообще сохранять? Конфиг пишется руками человеком для программы. Зачем самой программе в него писать?». И правда. Хорошенько подумав я не придумал нормальных вариантов использования, кроме уж сильно притянутых. На том и порешил что делать запись я не буду. Ни сейчас ни потом. Но вообще, это опенсорс и значит, что тот, кому понадобится — сможет и сам реализовать и прислать мне MR на почту. От такого я не откажусь! Лицензией я выбрал конечно же GPLv3. А что, тут есть выбор? Для меня есть только GPL. Остальные митоапачи — профанация и не интересно. [^4]: https://pkg.go.dev/go.neonxp.ru/conf@v1.0.1#pkg-index # POSE [Гит проекта](https://git.neonxp.ru/pose/) Я упомянул выше, что что я уже написал первый проект, где обкатал новую библиотеку. И этот проект - простая утилита, которая транслирует записи из источника (источников) в целевой сервис (сервисы). Ну то есть, из RSS/Atom в телеграм (на момент написания поста, 14.03.2026 не запрещённый на территории России). Хоть эта утилита уже работает у меня на сервере (транслирует Atom ленту этого блога в мой канал), я её воспринимаю скорее как референсный пример использования библиотеки конфигов. Так что да, если заинтересовались библиотекой conf - рекомендую посмотреть [этот проект](https://git.neonxp.ru/pose/tree/internal/application/application.go#n25) и [его конфиг](https://git.neonxp.ru/pose/tree/config.conf) как референсный пример использования библиотеки conf. Пожалуй, на этом пока всё. Если что-то не написал или непонятно — приглашаю обсудить со мной [по почте](mailto:i@neonxp.ru) или в комментариях ниже.