diff options
Diffstat (limited to '')
| -rw-r--r-- | README.md | 431 |
1 files changed, 331 insertions, 100 deletions
@@ -1,6 +1,9 @@ # conf -Go библиотека для парсинга конфигурационных файлов `.conf`. +Go библиотека для парсинга конфигурационных файлов `.conf` похожем на +классические UNIX конфиги, как у nginx или bind9. + +[English version](#english) ## Установка @@ -10,13 +13,12 @@ go get go.neonxp.ru/conf ## Особенности формата -- **Присваивания**: `key = value;` -- **Типы значений**: строки (двойные/одинарные кавычки, backticks), числа (целые/дробные), булевы значения -- **Директивы**: `directive arg1 arg2;` -- **Блочные директивы**: `directive { ... }` +- **Команды**: `directive arg1 arg2;` +- **Команды с телом**: `directive arg1 arg2 { ... }` +- **Типы аргументов**: строки (двойные/одинарные кавычки/backticks для многострочных строк), числа (целые/дробные), булевы значения +- **Вложенные блоки**: произвольная глубина вложенности - **Комментарии**: `#` до конца строки - **UTF-8**: включая кириллицу -- **Подстановка переменных окружения**: `$VAR` ## Быстрый старт @@ -26,68 +28,64 @@ package main import ( "fmt" "go.neonxp.ru/conf" - "go.neonxp.ru/conf/model" - "go.neonxp.ru/conf/visitor" ) func main() { - cfg := conf.New() - if err := cfg.LoadFile("config.conf"); err != nil { - panic(err) - } - - v := visitor.NewDefault() - if err := cfg.Process(v); err != nil { + // Загрузка из файла + cfg, err := conf.LoadFile("config.conf") + if err != nil { panic(err) } - // Доступ по пути с точечной нотацией - val, err := v.Get("server.host") - if err != nil { - panic(err) + // Получение команды и её значения + if hostCmd := cfg.Get("server"); hostCmd != nil { + fmt.Printf("Server: %v\n", hostCmd.Value()) } - fmt.Println(val.String()) // localhost - port, err := v.Get("server.port") - fmt.Println(port.Int()) // 8080 + // Навигация по вложенной структуре + sslEnabled := cfg.Get("server").Group.Get("ssl").Group.Get("enabled") + fmt.Printf("SSL enabled: %v\n", sslEnabled.Value()) } ``` ## Пример конфигурационного файла ```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"; -} +# Простые команды без тела +listen 8080; +host "127.0.0.1"; +debug false; -# Вложенные блоки -server { - host = "localhost"; - port = 8080; +# Команды с аргументами и телом +server "web" { + host "localhost"; + port 8080; ssl { - enabled = true; - cert = "/etc/ssl/cert.pem"; + enabled true; + cert "/etc/ssl/cert.pem"; + key "/etc/ssl/key.pem"; } middleware "auth" { - enabled = true; - secret = "$JWT_SECRET"; + enabled true; + secret "secret123"; } } +# Несколько команд с одинаковым именем +cache "redis" { + host "redis.local"; + port 6379; +} + +cache "memcached" { + host "memcached.local"; + port 11211; +} + # Многострочные строки -template = ` +template ` <!DOCTYPE html> <html> <body>Hello</body> @@ -100,93 +98,106 @@ template = ` ### Загрузка конфигурации ```go -cfg := conf.New() - // Из файла -cfg.LoadFile("config.conf") +cfg, err := conf.LoadFile("path/to/config.conf") // Из памяти -cfg.Load("inline", []byte("key = value;")) +cfg, err := conf.Load("inline", []byte("listen 8080;")) ``` -### Обработка через Visitor - -Библиотека использует паттерн Visitor для обхода конфигурации: +### Типы данных ```go -type Visitor interface { - VisitDirective(ident string, args Values, body Body) error - VisitSetting(key string, values Values) error +// model.Ident — идентификатор команды (псевдоним для string) +ident := model.Ident("server") + +// model.Command — команда с именем, аргументами и вложенной группой +type Command struct { + Name Ident // Имя команды + Args []any // Аргументы команды + Group Group // Вложенные команды (тело команды) } + +// model.Group — срез команд +type Group []Command ``` -### Get-методы на Values +### Методы Command -| Метод | Описание | -|-------|----------| -| `String()` | Строковое представление через пробел | -| `Int()` | Преобразование в int (одно значение) | -| `BuildString(lookups...)` | Сборка строки с подстановками | +| Метод | Описание | +| ------------- | -------------------------------------------- | +| `Value() any` | Возвращает первый аргумент команды или `nil` | -### Подстановка переменных окружения +### Методы Group -```go -vals, _ := v.Get("db_file") -path := vals.BuildString(model.LookupEnv) -// $HOME → "/home/user", результат: "/home/user/app/data.db" -``` +| Метод | Описание | +| ---------------------------------------------------------- | ---------------------------------------------------------- | +| `Get(name Ident) *Command` | Возвращает первую команду с указанным именем или `nil` | +| `Filter(predicate func(*Command) bool) iter.Seq[*Command]` | Возвращает итератор по командам, удовлетворяющим предикату | -### Кастомные подстановки +## Навигация по конфигурации ```go -substitutions := map[model.Word]string{ - "APP_DIR": "/opt/myapp", - "LOG_LEVEL": "debug", +// Простой доступ +listenCmd := cfg.Get("listen") +fmt.Println(listenCmd.Value()) // 8080 + +// Цепочка вложенной навигации +port := cfg.Get("server").Group.Get("http").Group.Get("port") +fmt.Println(port.Value()) // 8080 + +// Использование Filter +for cmd := range cfg.Filter(func(c *model.Command) bool { + return c.Name == "cache" +}) { + fmt.Printf("Cache: %v\n", cmd.Value()) } - -path := vals.BuildString(model.LookupSubst(substitutions), model.Origin) ``` -## Реализация собственного Visitor +## Работа с аргументами -```go -type MyVisitor struct{} +Аргументы сохраняются в срез `Args` типа `[]any`. Возможные типы: -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) // Рекурсивный обход тела -} +```go +// Строка в двойных кавычках: "value" +// Строка в одинарных кавычках: 'value' +// Строка в backticks: `value` +// Число целое: 42 +// Число дробное: 3.14 +// Булево значение: true, false + +// Пример доступа к аргументам: +cmd := cfg.Get("test") +if cmd != nil && len(cmd.Args) > 0 { + val1 := cmd.Args[0] // Первый аргумент + val2 := cmd.Args[1] // Второй аргумент + + // Приведение типа для строк + if str, ok := val1.(string); ok { + fmt.Println("String:", str) + } -func (m *MyVisitor) VisitSetting(key string, values model.Values) error { - fmt.Printf("Setting: %s = %s\n", key, values.String()) - return nil + // Приведение типа для чисел + if num, ok := val2.(int); ok { + fmt.Println("Int:", num) + } } ``` -## Грамматика (EBNF) +## Грамматика (PEG) -``` -Config = Doc . -Doc = Stmt { Stmt } . -Stmt = Word ( Assignment | Command ) . +Формат использует PEG (Parsing Expression Grammar). Грамматика описана в файле `parser/grammar.peg`. -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 = ";" . -``` +- Все команды без тела заканчиваются точкой с запятой `;` +- Тело команды заключается в фигурные скобки `{ }` +- Аргументы разделяются пробелами +- Комментарии начинаются с `#` ## Требования -- Go 1.25+ +- Go 1.23+ (для использования `iter.Seq`) ## Лицензия @@ -205,3 +216,223 @@ br = ";" . ## Автор - Александр Кирюхин <i@neonxp.ru> + +--- + +<a name="english"></a> + +# conf (English) + +Go library for parsing `.conf` configuration files (like many classic UNIX programms like nginx or bind9). + +## Installation + +```bash +go get go.neonxp.ru/conf +``` + +## Format Features + +- **Commands**: `directive arg1 arg2;` +- **Commands with body**: `directive arg1 arg2 { ... }` +- **Argument types**: strings (double/single quotes, backticks for multiline strings), numbers (integer/float), boolean values +- **Nested blocks**: arbitrary nesting depth +- **Comments**: `#` until end of line +- **UTF-8**: including Cyrillic + +## Quick Start + +```go +package main + +import ( + "fmt" + "go.neonxp.ru/conf" +) + +func main() { + // Load from file + cfg, err := conf.LoadFile("config.conf") + if err != nil { + panic(err) + } + + // Get command and its value + if hostCmd := cfg.Get("server"); hostCmd != nil { + fmt.Printf("Server: %v\n", hostCmd.Value()) + } + + // Navigate through nested structure + sslEnabled := cfg.Get("server").Group.Get("ssl").Group.Get("enabled") + fmt.Printf("SSL enabled: %v\n", sslEnabled.Value()) +} +``` + +## Example Configuration File + +```conf +# Simple commands without body +listen 8080; +host "127.0.0.1"; +debug false; + +# Commands with arguments and body +server "web" { + host "localhost"; + port 8080; + + ssl { + enabled true; + cert "/etc/ssl/cert.pem"; + key "/etc/ssl/key.pem"; + } + + middleware "auth" { + enabled true; + secret "secret123"; + } +} + +# Multiple commands with same name +cache "redis" { + host "redis.local"; + port 6379; +} + +cache "memcached" { + host "memcached.local"; + port 11211; +} + +# Multiline strings +template ` + <!DOCTYPE html> + <html> + <body>Hello</body> + </html> +`; +``` + +## API + +### Loading Configuration + +```go +// From file +cfg, err := conf.LoadFile("path/to/config.conf") + +// From memory +cfg, err := conf.Load("inline", []byte("listen 8080;")) +``` + +### Data Types + +```go +// model.Ident — command identifier (alias for string) +ident := model.Ident("server") + +// model.Command — command with name, arguments and nested group +type Command struct { + Name Ident // Command name + Args []any // Command arguments + Group Group // Nested commands (command body) +} + +// model.Group — slice of commands +type Group []Command +``` + +### Command Methods + +| Method | Description | +| ------------- | ------------------------------------------ | +| `Value() any` | Returns first argument of command or `nil` | + +### Group Methods + +| Method | Description | +| ---------------------------------------------------------- | -------------------------------------------------- | +| `Get(name Ident) *Command` | Returns first command with specified name or `nil` | +| `Filter(predicate func(*Command) bool) iter.Seq[*Command]` | Returns iterator over commands matching predicate | + +## Configuration Navigation + +```go +// Simple access +listenCmd := cfg.Get("listen") +fmt.Println(listenCmd.Value()) // 8080 + +// Nested navigation chain +port := cfg.Get("server").Group.Get("http").Group.Get("port") +fmt.Println(port.Value()) // 8080 + +// Using Filter +for cmd := range cfg.Filter(func(c *model.Command) bool { + return c.Name == "cache" +}) { + fmt.Printf("Cache: %v\n", cmd.Value()) +} +``` + +## Working with Arguments + +Arguments are stored in `Args` slice of type `[]any`. Possible types: + +```go +// Double-quoted string: "value" +// Single-quoted string: 'value' +// Backtick string: `value` +// Integer number: 42 +// Float number: 3.14 +// Boolean value: true, false + +// Example accessing arguments: +cmd := cfg.Get("test") +if cmd != nil && len(cmd.Args) > 0 { + val1 := cmd.Args[0] // First argument + val2 := cmd.Args[1] // Second argument + + // Type assertion for strings + if str, ok := val1.(string); ok { + fmt.Println("String:", str) + } + + // Type assertion for numbers + if num, ok := val2.(int); ok { + fmt.Println("Int:", num) + } +} +``` + +## Grammar (PEG) + +The format uses PEG (Parsing Expression Grammar). Grammar is described in file `parser/grammar.peg`. + +Basic rules: + +- All commands without body end with semicolon `;` +- Command body is enclosed in curly braces `{ }` +- Arguments are separated by spaces +- Comments start with `#` + +## Requirements + +- Go 1.23+ (for `iter.Seq`) + +## License + +This project is licensed under GNU General Public License version 3 (GPLv3). +See [LICENSE](LICENSE) file for details. + +``` + 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. +``` + +## Author + +- Alexander Kiryukhin <i@neonxp.ru> |
