diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-11 14:47:10 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-11 15:01:57 +0200 |
| commit | 42b02c47be9b285099203e44a2570636d4ca6f03 (patch) | |
| tree | 82bcb9e19bd886f36e1ca7b2d94a204c988f1fd1 /internal/config/main.go | |
| download | krino-42b02c47be9b285099203e44a2570636d4ca6f03.tar.gz krino-42b02c47be9b285099203e44a2570636d4ca6f03.zip | |
krino: foundation — sexp reader, config language, init/new/check
Diffstat (limited to 'internal/config/main.go')
| -rw-r--r-- | internal/config/main.go | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/internal/config/main.go b/internal/config/main.go new file mode 100644 index 0000000..ee6c3df --- /dev/null +++ b/internal/config/main.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package config + +import ( + "errors" + "path/filepath" + "regexp" + + "krino/internal/sexp" + "krino/internal/xdg" +) + +// Main is krino.conf. +type Main struct { + File string + Include []string // directory names, in run order + IncludePos map[string]sexp.Pos // where each name is written + // IncludeNode is the (include ...) form, kept so krino new can splice a + // name into it; nil when the file has none. + IncludeNode *sexp.Node + Log string // absolute; empty means the default + Defaults Settings +} + +// nameRE is what a directory name may look like: it becomes a file name. +var nameRE = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) + +// ParseMain reads the text of krino.conf. +func ParseMain(file string, src []byte) (*Main, []*Diag) { + m := &Main{File: file, IncludePos: map[string]sexp.Pos{}} + nodes, err := sexp.Parse(file, src) + if err != nil { + return m, []*Diag{fromSyntax(err)} + } + d := &diags{file: file} + seen := map[string]*sexp.Node{} + for _, n := range nodes { + head := n.Head() + if head == "" { + d.at(n, "expected a form like (include ...), got %s", n) + continue + } + if head == "include" || head == "log" || head == "defaults" { + if first, ok := seen[head]; ok { + d.at(n, "%s given twice (first at line %d)", head, first.Pos.Line) + continue + } + seen[head] = n + } + switch head { + case "include": + m.IncludeNode = n + for _, a := range n.Args() { + switch _, dup := m.IncludePos[a.Text]; { + case a.Kind != sexp.String: + d.at(a, `include takes directory names in quotes, like "downloads"; got %s`, a) + case !nameRE.MatchString(a.Text): + d.at(a, "bad directory name %q: use letters, digits, '.', '_' and '-'", a.Text) + case Reserved[a.Text]: + d.at(a, "%q is a krino command; choose another name", a.Text) + case dup: + d.at(a, "%q included twice", a.Text) + default: + m.Include = append(m.Include, a.Text) + m.IncludePos[a.Text] = a.Pos + } + } + case "log": + args := n.Args() + if len(args) != 1 || args[0].Kind != sexp.String { + d.at(n, `log takes one path in quotes, like (log "~/.local/state/krino/krino.log")`) + continue + } + p := xdg.Expand(args[0].Text) + if !filepath.IsAbs(p) { + d.at(args[0], "log path must be absolute or start with ~") + continue + } + m.Log = filepath.Clean(p) + case "defaults": + dseen := map[string]*sexp.Node{} + for _, a := range n.Args() { + if !isSetting(a.Head()) { + d.at(a, "defaults holds settings like (min-age 2m); got %s", a) + continue + } + m.Defaults.parse(a, d, dseen) + } + default: + d.at(n, "unknown form (%s ...); krino.conf has include, log and defaults", head) + } + } + return m, d.list +} + +// fromSyntax turns a reader error into a Diag. +func fromSyntax(err error) *Diag { + var se *sexp.Error + if errors.As(err, &se) { + return &Diag{File: se.File, Pos: se.Pos, Msg: se.Msg} + } + return &Diag{Msg: err.Error()} +} |
