kvconf
Flat key = value config files for Go, with live reload. Standard library
only, no dependencies.
# ~/.config/myapp/config
theme = dark
lock.after = 5m
border = #ff8800 # a value may start with #
title = "keeps # inside"
bind.cmd+q = close-window
output.DP-1.scale = 1.5
- Typed decoding into a struct with
kv tags: strings, bools (on/off),
numbers with bounds, durations, enums, lists, paths with ~/, maps for
dynamic keys (bind.*, output.<name>.<field>).
- Live reload with each OS's own notifications: inotify (Linux), kqueue
(macOS, BSD), ReadDirectoryChangesW (Windows), or polling.
- Safe for real configs: atomic saves, symlinked dotfiles, editors that
save by rename, missing directories, broken files that keep the last good
config, and warnings that never print values (they may be secrets).
- Fast: about 800 MB/s to parse, a few allocations per reported change.
go get github.com/bnema/kvconf
Quick start
type Config struct {
Theme string `kv:"theme" oneof:"dark|light"`
Lock time.Duration `kv:"lock.after,off" max:"1h"`
Shots kvconf.Path `kv:"screenshot.dir"`
Binds map[string]string `kv:"bind.*"`
}
func (c *Config) SetDefaults() { c.Theme, c.Lock = "dark", 5*time.Minute }
func main() {
path, _ := kvconf.DefaultPath("myapp", "config")
live, warnings, err := kvconf.Open(path, kvconf.LiveOptions[Config]{
OnChange: func(c kvconf.Change[Config]) { log.Println("reloaded", c.Keys) },
OnError: func(err error) { log.Println("config:", err) },
})
if err != nil {
log.Fatal(err)
}
for _, w := range warnings {
log.Println("config:", w) // "line 3: theme: must be dark or light"
}
go live.Run(ctx)
cfg := live.Get() // safe from any goroutine; never modified after
}
Without live reload: cfg, warnings, err := kvconf.Load[Config](path, kvconf.Options{}).
- One
key = value per line. Spaces around the key and value are trimmed.
# starts a comment at the start of a line, or after a space or a tab.
#ff8800 and a#b are values. See Options.Comments for other styles.
- A value that is exactly
"..." (Go escapes) or '...' (literal) is
unquoted. spawn "a b" stays as written.
- UTF-8, with an optional BOM and
\r\n line ends.
- A repeated key overrides the earlier one, with a warning.
| Tag |
Meaning |
kv:"name" |
The key. A nested struct prefixes its keys with name. |
kv:",inline" |
A nested struct whose keys are not prefixed |
kv:"name,off" |
off sets the zero value |
kv:"name,nonempty" |
An empty value is rejected |
kv:"bind.*" |
A map: bind.<name> = v. A map of structs takes bind.<name>.<field>; names may contain dots |
kv:"*" |
[]kvconf.Entry or map[string]string collecting unknown keys |
min:"1" max:"10" |
Bounds of a number or duration (max:"1h") |
oneof:"a|b" |
Allowed values |
sep:"|" |
List separator (default ,) |
Any type implementing encoding.TextUnmarshaler works as a field.
Validate() error on the config checks rules spanning several keys.
SetDefaults() fills defaults, also for each new element of a map of structs.
Errors and warnings
By default a bad line or value becomes a Warning with its line and key,
and the rest of the file still applies. Options.Strict rejects the whole
file instead. Options.AllowUnknown, AllowDuplicates, RequireFile,
MaxBytes (1 MiB) and MaxLine (64 KiB) tune the rest.
Live keeps the current config when the file is broken or removed, and
skips saves that change no value.
Lower-level API
Parse, ParseBytes: file to []Entry without decoding.
Decode, Unmarshal, Marshal: entries or bytes to and from a struct.
Watch(ctx, path, WatchOptions{...}, out): change notifications for one
file, or every file of a directory with Dir: true.
Set, SetFile: change keys in place, keeping comments and layout.
WriteFile: atomic replace, written through symlinks.
ReadFile: bounded read that rejects FIFOs and devices without blocking.
License
MIT
source
1# kvconf
2
3Flat `key = value` config files for Go, with live reload. Standard library
4only, no dependencies.
5
6```ini
7# ~/.config/myapp/config
8theme = dark
9lock.after = 5m
10border = #ff8800 # a value may start with #
11title = "keeps # inside"
12bind.cmd+q = close-window
13output.DP-1.scale = 1.5
14```
15
16- **Typed decoding** into a struct with `kv` tags: strings, bools (`on/off`),
17 numbers with bounds, durations, enums, lists, paths with `~/`, maps for
18 dynamic keys (`bind.*`, `output.<name>.<field>`).
19- **Live reload** with each OS's own notifications: inotify (Linux), kqueue
20 (macOS, BSD), ReadDirectoryChangesW (Windows), or polling.
21- **Safe for real configs**: atomic saves, symlinked dotfiles, editors that
22 save by rename, missing directories, broken files that keep the last good
23 config, and warnings that never print values (they may be secrets).
24- **Fast**: about 800 MB/s to parse, a few allocations per reported change.
25
26```sh
27go get github.com/bnema/kvconf
28```
29
30## Quick start
31
32```go
33type Config struct {
34 Theme string `kv:"theme" oneof:"dark|light"`
35 Lock time.Duration `kv:"lock.after,off" max:"1h"`
36 Shots kvconf.Path `kv:"screenshot.dir"`
37 Binds map[string]string `kv:"bind.*"`
38}
39
40func (c *Config) SetDefaults() { c.Theme, c.Lock = "dark", 5*time.Minute }
41
42func main() {
43 path, _ := kvconf.DefaultPath("myapp", "config")
44 live, warnings, err := kvconf.Open(path, kvconf.LiveOptions[Config]{
45 OnChange: func(c kvconf.Change[Config]) { log.Println("reloaded", c.Keys) },
46 OnError: func(err error) { log.Println("config:", err) },
47 })
48 if err != nil {
49 log.Fatal(err)
50 }
51 for _, w := range warnings {
52 log.Println("config:", w) // "line 3: theme: must be dark or light"
53 }
54 go live.Run(ctx)
55
56 cfg := live.Get() // safe from any goroutine; never modified after
57}
58```
59
60Without live reload: `cfg, warnings, err := kvconf.Load[Config](path, kvconf.Options{})`.
61
62## File format
63
64- One `key = value` per line. Spaces around the key and value are trimmed.
65- `#` starts a comment at the start of a line, or after a space or a tab.
66 `#ff8800` and `a#b` are values. See `Options.Comments` for other styles.
67- A value that is exactly `"..."` (Go escapes) or `'...'` (literal) is
68 unquoted. `spawn "a b"` stays as written.
69- UTF-8, with an optional BOM and `\r\n` line ends.
70- A repeated key overrides the earlier one, with a warning.
71
72## Tags
73
74| Tag | Meaning |
75| --- | --- |
76| `kv:"name"` | The key. A nested struct prefixes its keys with `name.` |
77| `kv:",inline"` | A nested struct whose keys are not prefixed |
78| `kv:"name,off"` | `off` sets the zero value |
79| `kv:"name,nonempty"` | An empty value is rejected |
80| `kv:"bind.*"` | A map: `bind.<name> = v`. A map of structs takes `bind.<name>.<field>`; names may contain dots |
81| `kv:"*"` | `[]kvconf.Entry` or `map[string]string` collecting unknown keys |
82| `min:"1" max:"10"` | Bounds of a number or duration (`max:"1h"`) |
83| `oneof:"a\|b"` | Allowed values |
84| `sep:"\|"` | List separator (default `,`) |
85
86Any type implementing `encoding.TextUnmarshaler` works as a field.
87`Validate() error` on the config checks rules spanning several keys.
88`SetDefaults()` fills defaults, also for each new element of a map of structs.
89
90## Errors and warnings
91
92By default a bad line or value becomes a `Warning` with its line and key,
93and the rest of the file still applies. `Options.Strict` rejects the whole
94file instead. `Options.AllowUnknown`, `AllowDuplicates`, `RequireFile`,
95`MaxBytes` (1 MiB) and `MaxLine` (64 KiB) tune the rest.
96
97`Live` keeps the current config when the file is broken or removed, and
98skips saves that change no value.
99
100## Lower-level API
101
102- `Parse`, `ParseBytes`: file to `[]Entry` without decoding.
103- `Decode`, `Unmarshal`, `Marshal`: entries or bytes to and from a struct.
104- `Watch(ctx, path, WatchOptions{...}, out)`: change notifications for one
105 file, or every file of a directory with `Dir: true`.
106- `Set`, `SetFile`: change keys in place, keeping comments and layout.
107- `WriteFile`: atomic replace, written through symlinks.
108- `ReadFile`: bounded read that rejects FIFOs and devices without blocking.
109
110## License
111
112MIT