-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathparser.go
195 lines (162 loc) · 4.97 KB
/
parser.go
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Copyright (C) 2022 Emanuele Rocca
//
// Pets configuration parser. Walk through a Pets directory and parse
// modelines.
package main
import (
"bufio"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
// Because it is important to know when enough is enough.
const MAXLINES int = 10
// ReadModelines looks into the given file and searches for pets modelines. A
// modeline is any string which includes the 'pets:' substring. All modelines
// found are returned as-is in a slice.
func ReadModelines(path string) ([]string, error) {
modelines := []string{}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
scannedLines := 0
for scanner.Scan() {
if scannedLines == MAXLINES {
return modelines, nil
}
line := scanner.Text()
if strings.Contains(line, "pets:") {
modelines = append(modelines, line)
}
scannedLines += 1
}
return modelines, nil
}
// ParseModeline parses a single pets modeline and populates the given PetsFile
// object. The line should something like:
// # pets: destfile=/etc/ssh/sshd_config, owner=root, group=root, mode=0644
func ParseModeline(line string, pf *PetsFile) error {
// We just ignore and throw away anything before the 'pets:' modeline
// identifier
re, err := regexp.Compile("pets:(.*)")
if err != nil {
return err
}
matches := re.FindStringSubmatch(line)
if len(matches) < 2 {
// We thought this was a pets modeline -- but then it turned out to be
// something different, very different indeed.
return fmt.Errorf("[ERROR] invalid pets modeline: %v", line)
}
components := strings.Split(matches[1], ",")
for _, comp := range components {
// Ignore whitespace
elem := strings.TrimSpace(comp)
if len(elem) == 0 || elem == "\t" {
continue
}
keyword, argument, found := strings.Cut(elem, "=")
// Just in case something bad should happen
badKeyword := fmt.Errorf("[ERROR] invalid keyword/argument '%v'", elem)
if !found {
return badKeyword // See? :(
}
switch keyword {
case "destfile":
pf.AddDest(argument)
case "symlink":
pf.AddLink(argument)
case "owner":
err = pf.AddUser(argument)
if err != nil {
log.Printf("[ERROR] unknown 'owner' %s, skipping directive\n", argument)
}
case "group":
err = pf.AddGroup(argument)
if err != nil {
log.Printf("[ERROR] unknown 'group' %s, skipping directive\n", argument)
}
case "mode":
pf.AddMode(argument)
case "package":
// haha gotcha this one has no setter
pf.Pkgs = append(pf.Pkgs, PetsPackage(argument))
case "pre":
pf.AddPre(argument)
case "post":
pf.AddPost(argument)
default:
return badKeyword
}
// :)
//log.Printf("[DEBUG] keyword '%v', argument '%v'\n", keyword, argument)
}
return nil
}
// ParseFiles walks the given directory, identifies all configuration files
// with pets modelines, and returns a list of parsed PetsFile(s).
func ParseFiles(directory string) ([]*PetsFile, error) {
var petsFiles []*PetsFile
log.Printf("[DEBUG] using configuration directory '%s'\n", directory)
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
// This function is called once for each file in the Pets configuration
// directory
if err != nil {
return err
}
if info.IsDir() {
// Skip directories
return nil
}
modelines, err := ReadModelines(path)
if err != nil {
// Returning the error we stop parsing all other files too. Debatable
// whether we want to do that here or not. ReadModelines should not
// fail technically, so it's probably fine to do it. Alternatively, we
// could just log to stderr and return nil like we do later on for
// syntax errors.
return err
}
if len(modelines) == 0 {
// Not a Pets file. We don't take it personal though
return nil
}
log.Printf("[DEBUG] %d pets modelines found in %s\n", len(modelines), path)
// Instantiate a PetsFile representation. The only thing we know so far
// is the source path. Every long journey begins with a single step!
pf := NewPetsFile()
// Get absolute path to the source. Technically we would be fine with a
// relative path too, but it's good to remove abiguity. Plus absolute
// paths make things easier in case we have to create a symlink.
abs, err := filepath.Abs(path)
if err != nil {
return err
}
pf.Source = abs
for _, line := range modelines {
err := ParseModeline(line, pf)
if err != nil {
// Possibly a syntax error, skip the whole file but do not return
// an error! Otherwise all other files will be skipped too.
log.Println(err) // XXX: log to stderr
return nil
}
}
if pf.Dest == "" {
// 'destfile' or 'symlink' are mandatory arguments. If we did not
// find any, consider it an error.
log.Println(fmt.Errorf("[ERROR] Neither 'destfile' nor 'symlink' directives found in '%s'", path))
return nil
}
log.Printf("[DEBUG] '%s' pets syntax OK\n", path)
petsFiles = append(petsFiles, pf)
return nil
})
return petsFiles, err
}