diff options
| author | 2026-04-03 15:40:30 +0300 | |
|---|---|---|
| committer | 2026-04-03 15:40:30 +0300 | |
| commit | ff79efd13b537d714dfacf74d72e84331eca39a3 (patch) | |
| tree | f6bd2cac5a34eb5011282dc82d04fcd44e016ab5 /content/posts/2026-03-14-conf/index.md | |
| parent | upd theme (diff) | |
| download | blog-master.tar.gz blog-master.tar.bz2 blog-master.tar.xz blog-master.zip | |
Diffstat (limited to '')
| -rw-r--r-- | content/posts/2026-03-14-conf/index.md | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/content/posts/2026-03-14-conf/index.md b/content/posts/2026-03-14-conf/index.md new file mode 100644 index 0000000..75236b8 --- /dev/null +++ b/content/posts/2026-03-14-conf/index.md @@ -0,0 +1,220 @@ +--- +title: Идеальный формат конфигов * +date: 2026-03-14T17:21:59+03:00 +tags: ["go", "conf"] +category: + - IT + - Мои проекты +--- + +\* лично для меня + +В общем, случилось и на неделе я таки присвоил тег v1 для своей самописной Go +библиотеки для разбора конфигов! Но обо всём по порядку. Или можно пропустить +предысторию и сразу [перейти к описанию библиотеки](#conf-v1). + +# Предыстория + +Около месяца назад я задумался написать небольшую утилиту для себя, которая бы +организовывала для меня рабочее окружение. Не важно сейчас, как именно должна +была организовывать, а важно, что эта утилита должна бы была иметь весьма +разухабистый конфиг вследствие своей планируемой гибкости. И встал вопрос, а +какой формат конфигов использовать? Казалось бы, возьми yaml, toml, на худой +конец, json (hjson, json5, итп). Даже думал об ini формате! Но всё было не то... + +И дело даже не в моём <abbr title="Not Invented Here">NIH</abbr> синдроме. +А они все мне не подходят! + +## 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) или в комментариях ниже. |
