summaryrefslogtreecommitdiff
path: root/content/posts/2026-03-14-conf/index.md
blob: 75236b8c75ef3682500674da64dfa836f42807b3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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) или в комментариях ниже.