1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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()}
}
|