~/NeonXP.log

Идеальный формат конфигов *

* лично для меня

В общем, случилось и на неделе я таки присвоил тег v1 для своей самописной Go библиотеки для разбора конфигов! Но обо всём по порядку. Или можно пропустить предысторию и сразу перейти к описанию библиотеки.

Предыстория

Около месяца назад я задумался написать небольшую утилиту для себя, которая бы организовывала для меня рабочее окружение. Не важно сейчас, как именно должна была организовывать, а важно, что эта утилита должна бы была иметь весьма разухабистый конфиг вследствие своей планируемой гибкости. И встал вопрос, а какой формат конфигов использовать? Казалось бы, возьми 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, немного помучался с API сгенерированного парсера, но потом всё же всё заработало! Кроме… Кроме того, что я наткнулся на неприятное свойство поведения: если парсер натыкается на неожиданный символ — он выдавал непонятную без полулитра ошибку вида “index of array out of range”. Причём без номера строки и символа. Сиди и гадай, что пошло не так. Убив на это без малого пару дней, я так и не смог сделать так, чтобы ошибка была более человеческая (типа «строка 2 символ 4: ожидалось что-то, а тут что-то другое»). Поэтому я принял волевое решение переписать совсем с нуля. Взял другой генератор парсеров, переписал грамматику c EBNF на PEG 2 и … И получилось гораздо более элегантно, чем с egg! Счастью моему не было предела, когда я получил первый успех! Да, конечно, потом пара дней полировки и обвешивания необязательными, но приятными фичами и готово! После того как я попробовал на практике свою библиотеку в одном простеньком проекте (о нём в конце) — я с чистой совестью присвоил библиотеке тег стабильной версии, т.к. я получил то, что хотел и больше править её в ближайшее время я не собираюсь.

conf v1

Встречайте:

Как я уже говорил, синтаксис очень простой. Для наглядности я приведу сразу пример, который покажет, по сути, все возможности. Да, возможностей не много, потому что я ценю минимализм, как уже говорил выше.

# Две одноименных директивы
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 Хер вы меня заставите передумать!

И всё! Просто и очень наглядно. Идеально для конфигов!

Использование в Go

Естественно, не могло быть и речи о анмаршалинге этого формата на структуры, как это делается у JSON YAML и прочих. Но это и не надо! У библиотеки есть несколько встроенных типов, таких как:

  • Group - группа директив. В том числе и тело директивы. Всё просто! Из методов есть базовые методы для получения конкретных директив из группы и простой фильтр.
  • Directive - для директив. У него есть несколько методов для типизированного получения первого из аргументов директивы (того что после имени директивы), также метод получения всех аргументов и тела. Так же есть и специальный метод StringExt3, который сливает все аргументы в одну строку с разделителем sep и пропуская аргументы типа Ident через переданную функцию identLookup.

Это два самых главных типа. Помимо них есть ещё и Ident о котором я говорил выше и тип Lookup, который определяет функцию подстановки для метода StringExt, намеренно сделанный совместимым со стандартным os.LookupEnv.

Я постарался очень и очень поверхностно дать описание API, т.к. можно подробно прочитать об API и на pkg.go.dev.

А чего же не хватает у в этом API? Записи конфига! Да! Есть только Load и LoadFile4, но нет никакого Write, Marshal и чего-то такого! Я долго думал, как это сделать. Ведь всё таки если делать запись, то по хорошему надо следить за тем, чтобы и комментарии сохранялись, причём строго там, где они были в оригинальном конфиге. Более того, по хорошему, нужно сделать так, чтобы после LoadFile → WriteFile полученный файл должен побайтово совпадать с тем что было. Да, дохрена забот! А потом я подумал «А зачем мне вообще сохранять? Конфиг пишется руками человеком для программы. Зачем самой программе в него писать?». И правда. Хорошенько подумав я не придумал нормальных вариантов использования, кроме уж сильно притянутых. На том и порешил что делать запись я не буду. Ни сейчас ни потом. Но вообще, это опенсорс и значит, что тот, кому понадобится — сможет и сам реализовать и прислать мне MR на почту. От такого я не откажусь!

Лицензией я выбрал конечно же GPLv3. А что, тут есть выбор? Для меня есть только GPL. Остальные митоапачи — профанация и не интересно.

POSE

Гит проекта

Я упомянул выше, что что я уже написал первый проект, где обкатал новую библиотеку. И этот проект - простая утилита, которая транслирует записи из источника (источников) в целевой сервис (сервисы). Ну то есть, из RSS/Atom в телеграм (на момент написания поста, 14.03.2026 не запрещённый на территории России). Хоть эта утилита уже работает у меня на сервере (транслирует Atom ленту этого блога в мой канал), я её воспринимаю скорее как референсный пример использования библиотеки конфигов.

Так что да, если заинтересовались библиотекой conf - рекомендую посмотреть этот проект и его конфиг как референсный пример использования библиотеки conf.

Пожалуй, на этом пока всё. Если что-то не написал или непонятно — приглашаю обсудить со мной по почте или в комментариях ниже.

Теги:

Комментарии

Комментариев пока нет.
Для отправки комментария достаточно отправить e-mail со своим комментарием на адрес blog@neonxp.ru, в теме нужно указать ссылку на пост.
Или просто нажать кнопку ниже. Всё очень просто :)
Написать комментарий