aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2026-02-22 20:15:50 +0300
committer2026-02-22 20:15:50 +0300
commitdb8bb97dfa2dacef002a1f349ea970d76fee4fc9 (patch)
tree7de11be3a01a6ef83a218dc98d90586dd1afb09a
parentДобавил утилитарные функции для моделей (diff)
downloadconf-0.0.4.tar.gz
conf-0.0.4.tar.bz2
conf-0.0.4.tar.xz
conf-0.0.4.zip
Refactoringv0.0.4
-rw-r--r--README.md211
-rw-r--r--example/file2.conf7
-rw-r--r--example/main.go15
-rw-r--r--internal/ast/processor.go26
-rw-r--r--loader.go30
-rw-r--r--loader_test.go35
-rw-r--r--model/body.go28
-rw-r--r--model/command.go9
-rw-r--r--model/directive.go9
-rw-r--r--model/doc.go41
-rw-r--r--model/setting.go (renamed from model/assignment.go)2
-rw-r--r--model/visitor.go6
-rw-r--r--visitor/default.go63
13 files changed, 341 insertions, 141 deletions
diff --git a/README.md b/README.md
index 1c0eb9e..267b226 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# conf
-Go библиотека для чтения конфигурационных файлов в формате `.conf`.
+Go библиотека для парсинга конфигурационных файлов `.conf`.
## Установка
@@ -10,17 +10,15 @@ go get go.neonxp.ru/conf
## Особенности формата
-- Простые присваивания: `key = value;`
-- Строковые значения: `"text"` или `'text'`
-- Многострочные строки: ``` `text` ```
-- Числовые значения: целые и дробные, включая отрицательные
-- Булевы значения: `true` / `false`
-- Директивы/команды: `directive arg1 arg2;`
-- Групповые директивы с блоками кода: `directive { ... }`
-- Комментарии от `#` до конца строки
-- Поддержка кириллицы и UTF-8
+- **Присваивания**: `key = value;`
+- **Типы значений**: строки (двойные/одинарные кавычки, backticks), числа (целые/дробные), булевы значения
+- **Директивы**: `directive arg1 arg2;`
+- **Блочные директивы**: `directive { ... }`
+- **Комментарии**: `#` до конца строки
+- **UTF-8**: включая кириллицу
+- **Подстановка переменных окружения**: `$VAR`
-## Пример использования
+## Быстрый старт
```go
package main
@@ -28,86 +26,163 @@ package main
import (
"fmt"
"go.neonxp.ru/conf"
+ "go.neonxp.ru/conf/model"
+ "go.neonxp.ru/conf/visitor"
)
func main() {
- doc, err := conf.LoadFile("./config.conf")
- if err != nil {
+ cfg := conf.New()
+ if err := cfg.LoadFile("config.conf"); err != nil {
panic(err)
}
- // Получение значения по ключу
- values := doc.Get("my_key")
- fmt.Println(values)
-
- // Получение команд по имени
- commands := doc.Commands("directivename")
- fmt.Println(commands)
+ v := visitor.NewDefault()
+ if err := cfg.Process(v); err != nil {
+ panic(err)
+ }
- // Все переменные
- vars := doc.Vars()
+ // Доступ по пути с точечной нотацией
+ val, err := v.Get("server.host")
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(val.String()) // localhost
- // Все элементы документа
- items := doc.Items()
+ port, err := v.Get("server.port")
+ fmt.Println(port.Int()) // 8080
}
```
## Пример конфигурационного файла
```conf
-# Пример конфигурации
+# Переменная окружения: db_file = $HOME "/app/data.db";
+
+# Простые присваивания
+rss = "https://neonxp.ru/feed/";
+host = "localhost";
+port = 8080;
+debug = true;
+
+# Директивы с аргументами
+telegram "bot123" "-1003888840756" {
+ token = "token_value";
+ admin_chat = "@admin";
+}
+
+# Вложенные блоки
+server {
+ host = "localhost";
+ port = 8080;
-# Простое присваивание
-simple_key = value;
+ ssl {
+ enabled = true;
+ cert = "/etc/ssl/cert.pem";
+ }
-# Многострочное присваивание
-string_key =
- "value"
- 'string';
+ middleware "auth" {
+ enabled = true;
+ secret = "$JWT_SECRET";
+ }
+}
-# Многострочные строки (backticks)
-multiline_string = `
- multiline
- string
- 123
+# Многострочные строки
+template = `
+ <!DOCTYPE html>
+ <html>
+ <body>Hello</body>
+ </html>
`;
+```
+
+## API
-# Числа и булевы значения
-int_key = -123.456;
-bool_key = true;
+### Загрузка конфигурации
-# Директивы
-expression1 argument1 "argument2" 123;
+```go
+cfg := conf.New()
-# Групповая директива
-group_directive_without_arguments {
- expression1 argument2 "string" 123 true;
- expression2 argument3 "string111" 123321 false;
+// Из файла
+cfg.LoadFile("config.conf")
- children_group "some argument" {
- # Вложенная группа
- }
+// Из памяти
+cfg.Load("inline", []byte("key = value;"))
+```
+
+### Обработка через Visitor
+
+Библиотека использует паттерн Visitor для обхода конфигурации:
+
+```go
+type Visitor interface {
+ VisitDirective(ident string, args Values, body Body) error
+ VisitSetting(key string, values Values) error
}
+```
-# Групповая директива с аргументами
-group_directive_with_argument "argument1" 'argument2' {
- child_val = "children value";
+### Get-методы на Values
+
+| Метод | Описание |
+|-------|----------|
+| `String()` | Строковое представление через пробел |
+| `Int()` | Преобразование в int (одно значение) |
+| `BuildString(lookups...)` | Сборка строки с подстановками |
+
+### Подстановка переменных окружения
+
+```go
+vals, _ := v.Get("db_file")
+path := vals.BuildString(model.LookupEnv)
+// $HOME → "/home/user", результат: "/home/user/app/data.db"
+```
+
+### Кастомные подстановки
+
+```go
+substitutions := map[model.Word]string{
+ "APP_DIR": "/opt/myapp",
+ "LOG_LEVEL": "debug",
}
+
+path := vals.BuildString(model.LookupSubst(substitutions), model.Origin)
```
-## API
+## Реализация собственного Visitor
-### Функции
+```go
+type MyVisitor struct{}
-- `LoadFile(filename string) (*model.Doc, error)` - загрузка конфигурации из файла
-- `Load(name string, input []byte) (*model.Doc, error)` - парсинг конфигурации из байтов
+func (m *MyVisitor) VisitDirective(ident string, args model.Values, body model.Body) error {
+ fmt.Printf("Directive: %s, args: %s\n", ident, args.String())
+ return body.Execute(m) // Рекурсивный обход тела
+}
-### Методы `*model.Doc`
+func (m *MyVisitor) VisitSetting(key string, values model.Values) error {
+ fmt.Printf("Setting: %s = %s\n", key, values.String())
+ return nil
+}
+```
+
+## Грамматика (EBNF)
-- `Get(key string) Values` - получить значения по ключу
-- `Commands(name string) Commands` - получить команды по имени
-- `Vars() map[string]Values` - получить все переменные
-- `Items() []any` - получить все элементы документа
+```
+Config = Doc .
+Doc = Stmt { Stmt } .
+Stmt = Word ( Assignment | Command ) .
+
+Assignment = "=" Values br .
+Command = [Values] ( Body | br ) .
+
+Values = Value { Value } .
+Value = Word | String | Number | Boolean .
+Body = "{" [ Doc ] "}" .
+
+Word = word (alpha | "$" | "_") {alpha | number | "$" | "_"} .
+String = `"[^"]*"` | `'[^']*'` | '`' { `[^`]' } '`' .
+Number = `-?[0-9]+(\.[0-9]+)?` .
+Boolean = `true` | `false` .
+br = ";" .
+```
## Требования
@@ -115,4 +190,18 @@ group_directive_with_argument "argument1" 'argument2' {
## Лицензия
-См. файл [LICENSE](LICENSE)
+Этот проект лицензирован в соответствии с GNU General Public License версии 3
+(GPLv3). Подробности смотрите в файле [LICENSE](LICENSE).
+
+```
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2026 Alexander NeonXP Kiryukhin <i@neonxp.ru>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+```
+
+## Автор
+
+- Александр Кирюхин <i@neonxp.ru>
diff --git a/example/file2.conf b/example/file2.conf
new file mode 100644
index 0000000..b22f06b
--- /dev/null
+++ b/example/file2.conf
@@ -0,0 +1,7 @@
+rss = "https://neonxp.ru/feed/";
+db_file = $XDG_DATA_HOME "/pose/pose.db";
+
+telegram {
+ token = "279146841:AAsome_secret_token_M";
+ groups = "-1003888840756";
+}
diff --git a/example/main.go b/example/main.go
index 6e0c8b8..c42ab3b 100644
--- a/example/main.go
+++ b/example/main.go
@@ -4,13 +4,24 @@ import (
"fmt"
"go.neonxp.ru/conf"
+ "go.neonxp.ru/conf/visitor"
)
func main() {
- out, err := conf.LoadFile("./file.conf")
+ cfg := conf.New()
+ if err := cfg.LoadFile("./example/file2.conf"); err != nil {
+ panic(err)
+ }
+
+ pr := visitor.NewDefault()
+ if err := cfg.Process(pr); err != nil {
+ panic(err)
+ }
+
+ tok, err := pr.Get("telegram.token")
if err != nil {
panic(err)
}
- fmt.Println(out)
+ fmt.Println(tok.String())
}
diff --git a/internal/ast/processor.go b/internal/ast/processor.go
index 783dbbe..55dce59 100644
--- a/internal/ast/processor.go
+++ b/internal/ast/processor.go
@@ -8,7 +8,7 @@ import (
"go.neonxp.ru/conf/model"
)
-func ToDoc(config *Node) (*model.Doc, error) {
+func ToDoc(config *Node) (model.Body, error) {
if len(config.Children) < 1 {
return nil, fmt.Errorf("invalid ast tree")
}
@@ -18,31 +18,31 @@ func ToDoc(config *Node) (*model.Doc, error) {
return processDoc(doc), nil
}
-func processDoc(docNode *Node) *model.Doc {
- doc := model.New(len(docNode.Children))
+func processDoc(docNode *Node) model.Body {
+ doc := make(model.Body, 0, len(docNode.Children))
for _, stmt := range docNode.Children {
- processStmt(doc, stmt)
+ processStmt(&doc, stmt)
}
return doc
}
-func processStmt(doc *model.Doc, stmt *Node) {
+func processStmt(doc *model.Body, stmt *Node) {
ident := extractIdent(stmt.Children[0])
nodeBody := stmt.Children[1]
switch nodeBody.Symbol {
case parser.Command:
- doc.AppendCommand(processCommand(ident, nodeBody))
+ *doc = append(*doc, processDirective(ident, nodeBody))
case parser.Assignment:
- doc.AppendAssignment(processAssignment(ident, nodeBody))
+ *doc = append(*doc, processSetting(ident, nodeBody))
}
}
-func processCommand(ident string, command *Node) *model.Command {
- result := &model.Command{
+func processDirective(ident string, directive *Node) *model.Directive {
+ result := &model.Directive{
Name: ident,
}
- for _, child := range command.Children {
+ for _, child := range directive.Children {
// Can be arguments OR body OR both
switch child.Symbol {
case parser.Values:
@@ -56,10 +56,10 @@ func processCommand(ident string, command *Node) *model.Command {
return result
}
-func processAssignment(ident string, assignment *Node) *model.Assignment {
- result := &model.Assignment{
+func processSetting(ident string, setting *Node) *model.Setting {
+ result := &model.Setting{
Key: ident,
- Value: extractValues(assignment.Children[1]), // Children[0] = '=', Children[1] = Values, Children[2] = ';'
+ Value: extractValues(setting.Children[1]), // Children[0] = '=', Children[1] = Values, Children[2] = ';'
}
return result
diff --git a/loader.go b/loader.go
index e9757a6..7f58237 100644
--- a/loader.go
+++ b/loader.go
@@ -9,28 +9,44 @@ import (
"go.neonxp.ru/conf/model"
)
-func LoadFile(filename string) (*model.Doc, error) {
+func New() *Conf {
+ return &Conf{
+ root: model.Body{},
+ }
+}
+
+type Conf struct {
+ root model.Body
+}
+
+func (c *Conf) LoadFile(filename string) error {
content, err := os.ReadFile(filename)
if err != nil {
- return nil, fmt.Errorf("failed load file: %w", err)
+ return fmt.Errorf("failed load file: %w", err)
}
- return Load(filename, content)
+ return c.Load(filename, content)
}
-func Load(name string, input []byte) (*model.Doc, error) {
+func (c *Conf) Load(name string, input []byte) error {
p := &parser.Parser{}
astSlice, err := p.Parse(name, input)
if err != nil {
- return nil, fmt.Errorf("failed parse conf content: %w", err)
+ return fmt.Errorf("failed parse conf content: %w", err)
}
astTree := ast.Parse(p, astSlice)
doc, err := ast.ToDoc(astTree[0])
if err != nil {
- return nil, fmt.Errorf("failed build Doc: %w", err)
+ return fmt.Errorf("failed build Doc: %w", err)
}
- return doc, nil
+ c.root = doc
+
+ return nil
+}
+
+func (c *Conf) Process(visitor model.Visitor) error {
+ return c.root.Execute(visitor)
}
diff --git a/loader_test.go b/loader_test.go
index acd689d..3e16922 100644
--- a/loader_test.go
+++ b/loader_test.go
@@ -4,9 +4,10 @@ import (
"fmt"
"go.neonxp.ru/conf"
+ "go.neonxp.ru/conf/visitor"
)
-func ExampleLoad() {
+func ExampleNew() {
config := `
key = "value";
group "test" {
@@ -14,17 +15,37 @@ func ExampleLoad() {
}
`
- cfg, err := conf.Load("example", []byte(config))
+ cfg := conf.New()
+
+ if err := cfg.Load("example", []byte(config)); err != nil {
+ panic(err)
+ }
+
+ pr := visitor.NewDefault()
+ if err := cfg.Process(pr); err != nil {
+ panic(err)
+ }
+
+ val1, err := pr.Get("key")
+ if err != nil {
+ panic(err)
+ }
+ val2, err := pr.Get("group.key")
if err != nil {
panic(err)
}
- fmt.Println("key =", cfg.Get("key")[0])
- group := cfg.Commands("group")
- for _, gr := range group {
- fmt.Println("key =", gr.Body.Get("key")[0])
+ val3, err := pr.Get("group")
+ if err != nil {
+ panic(err)
}
+
+ fmt.Println("key =", val1.String())
+ fmt.Println("group.key =", val2.String())
+ fmt.Println("group args =", val3.String())
+
// Output:
// key = value
- // key = 123
+ // group.key = 123
+ // group args = test
}
diff --git a/model/body.go b/model/body.go
new file mode 100644
index 0000000..b7b4a0c
--- /dev/null
+++ b/model/body.go
@@ -0,0 +1,28 @@
+package model
+
+import (
+ "errors"
+ "fmt"
+)
+
+type Body []any
+
+var ErrInvalidType = errors.New("invalid type")
+
+func (d Body) Execute(v Visitor) error {
+ for _, it := range d {
+ switch it := it.(type) {
+ case *Setting:
+ if err := v.VisitSetting(it.Key, it.Value); err != nil {
+ return err
+ }
+ case *Directive:
+ if err := v.VisitDirective(it.Name, it.Arguments, it.Body); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("%w: %t", ErrInvalidType, it)
+ }
+ }
+ return nil
+}
diff --git a/model/command.go b/model/command.go
deleted file mode 100644
index 237e94f..0000000
--- a/model/command.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package model
-
-type Command struct {
- Name string
- Arguments Values
- Body *Doc
-}
-
-type Commands []*Command
diff --git a/model/directive.go b/model/directive.go
new file mode 100644
index 0000000..3852e72
--- /dev/null
+++ b/model/directive.go
@@ -0,0 +1,9 @@
+package model
+
+type Directive struct {
+ Name string
+ Arguments Values
+ Body Body
+}
+
+type Directives []*Directive
diff --git a/model/doc.go b/model/doc.go
deleted file mode 100644
index 9c13a4c..0000000
--- a/model/doc.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package model
-
-type Doc struct {
- items []any
- vars map[string]Values
- commands map[string]Commands
-}
-
-func New(cap int) *Doc {
- return &Doc{
- items: make([]any, 0, cap),
- vars: make(map[string]Values, cap),
- commands: make(map[string]Commands, cap),
- }
-}
-
-func (d *Doc) AppendAssignment(e *Assignment) {
- d.items = append(d.items, e)
- d.vars[e.Key] = append(d.vars[e.Key], e.Value...)
-}
-
-func (d *Doc) AppendCommand(c *Command) {
- d.items = append(d.items, c)
- d.commands[c.Name] = append(d.commands[c.Name], c)
-}
-
-func (d *Doc) Vars() map[string]Values {
- return d.vars
-}
-
-func (d *Doc) Get(key string) Values {
- return d.vars[key]
-}
-
-func (d *Doc) Commands(name string) Commands {
- return d.commands[name]
-}
-
-func (d *Doc) Items() []any {
- return d.items
-}
diff --git a/model/assignment.go b/model/setting.go
index 746e2b3..363a8a9 100644
--- a/model/assignment.go
+++ b/model/setting.go
@@ -1,6 +1,6 @@
package model
-type Assignment struct {
+type Setting struct {
Key string
Value Values
}
diff --git a/model/visitor.go b/model/visitor.go
new file mode 100644
index 0000000..3f290d3
--- /dev/null
+++ b/model/visitor.go
@@ -0,0 +1,6 @@
+package model
+
+type Visitor interface {
+ VisitDirective(ident string, args Values, body Body) error
+ VisitSetting(key string, values Values) error
+}
diff --git a/visitor/default.go b/visitor/default.go
new file mode 100644
index 0000000..d8c8172
--- /dev/null
+++ b/visitor/default.go
@@ -0,0 +1,63 @@
+package visitor
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "go.neonxp.ru/conf/model"
+)
+
+var (
+ ErrEmptyQuery = errors.New("empty query")
+ ErrNoChildKey = errors.New("no child key")
+)
+
+func NewDefault() *Default {
+ return &Default{
+ vars: map[string]model.Values{},
+ children: map[string]*Default{},
+ args: model.Values{},
+ }
+}
+
+// Default просто собирает рекурсивно все переменные в дерево.
+// На самом деле, для большинства сценариев конфигов его должно хватить.
+type Default struct {
+ vars map[string]model.Values
+ children map[string]*Default
+ args model.Values
+}
+
+func (p *Default) VisitDirective(ident string, args model.Values, body model.Body) error {
+ p.children[ident] = NewDefault()
+ p.children[ident].args = args
+ return body.Execute(p.children[ident])
+}
+
+func (p *Default) VisitSetting(key string, values model.Values) error {
+ p.vars[key] = values
+
+ return nil
+}
+
+func (p *Default) Get(path string) (model.Values, error) {
+ splitPath := strings.SplitN(path, ".", 2)
+ switch len(splitPath) {
+ case 1:
+ if v, ok := p.vars[splitPath[0]]; ok {
+ return v, nil
+ }
+ if child, ok := p.children[splitPath[0]]; ok {
+ return child.args, nil
+ }
+ return nil, fmt.Errorf("%w: %s", ErrNoChildKey, splitPath[0])
+ case 2:
+ if child, ok := p.children[splitPath[0]]; ok {
+ return child.Get(splitPath[1])
+ }
+ return nil, fmt.Errorf("%w: %s", ErrNoChildKey, splitPath[0])
+ default:
+ return nil, ErrEmptyQuery
+ }
+}