summaryrefslogtreecommitdiff
path: root/content/posts/2026-03-14-conf/index.md
diff options
context:
space:
mode:
author2026-04-03 15:40:30 +0300
committer2026-04-03 15:40:30 +0300
commitff79efd13b537d714dfacf74d72e84331eca39a3 (patch)
treef6bd2cac5a34eb5011282dc82d04fcd44e016ab5 /content/posts/2026-03-14-conf/index.md
parentupd theme (diff)
downloadblog-ff79efd13b537d714dfacf74d72e84331eca39a3.tar.gz
blog-ff79efd13b537d714dfacf74d72e84331eca39a3.tar.bz2
blog-ff79efd13b537d714dfacf74d72e84331eca39a3.tar.xz
blog-ff79efd13b537d714dfacf74d72e84331eca39a3.zip
обновленияHEADmaster
Diffstat (limited to 'content/posts/2026-03-14-conf/index.md')
-rw-r--r--content/posts/2026-03-14-conf/index.md220
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) или в комментариях ниже.