// SPDX-License-Identifier: GPL-3.0-or-later package config import ( "errors" "path/filepath" "regexp" "git.labunix.xyz/krino/internal/sexp" "git.labunix.xyz/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 Excludes []*Exclude // apply to every directory, before its own } // 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) } case "exclude": if x := parseExclude(n, src, d); x != nil { m.Excludes = append(m.Excludes, x) } default: d.at(n, "unknown form (%s ...); krino.conf has include, log, defaults and exclude", 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()} }