summaryrefslogtreecommitdiff
path: root/internal/config/main.go
blob: 2764c8af1fb4025016c7900346585c6ead107a22 (plain) (blame)
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
105
106
107
108
109
// 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
	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()}
}