aboutsummaryrefslogtreecommitdiff
path: root/internal/config/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config/main.go')
-rw-r--r--internal/config/main.go104
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()}
+}