forked from ohpauleez/go-mk
-
Notifications
You must be signed in to change notification settings - Fork 1
/
rules.go
215 lines (186 loc) · 4.97 KB
/
rules.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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// Mkfiles are parsed into ruleSets, which as the name suggests, are sets of
// rules with accompanying recipes, as well as assigned variables which are
// expanding when evaluating rules and recipes.
package main
import (
"fmt"
"regexp"
"unicode/utf8"
)
type attribSet struct {
delFailed bool // delete targets when the recipe fails
nonstop bool // don't stop if the recipe fails
forcedTimestamp bool // update timestamp whether the recipe does or not
nonvirtual bool // a meta-rule that will only match files
quiet bool // don't print the recipe
regex bool // regular expression meta-rule
update bool // treat the targets as if they were updated
virtual bool // rule is virtual (does not match files)
exclusive bool // don't execute concurrently with any other rule
}
// Error parsing an attribute
type attribError struct {
found rune
}
// target and rereq patterns
type pattern struct {
issuffix bool // is a suffix '%' rule, so we should define $stem.
spat string // simple string pattern
rpat *regexp.Regexp // non-nil if this is a regexp pattern
}
// Match a pattern, returning an array of submatches, or nil if it doesn'm
// match.
func (p *pattern) match(target string) []string {
if p.rpat != nil {
return p.rpat.FindStringSubmatch(target)
}
if target == p.spat {
return make([]string, 0)
}
return nil
}
// A single rule.
type rule struct {
targets []pattern // non-empty array of targets
attributes attribSet // rule attributes
ismeta bool // is this a meta rule
prereqs []string // possibly empty prerequesites
shell []string // command used to execute the recipe
recipe string // recipe source
command []string // command attribute
file string // file where the rule is defined
line int // line number on which the rule is defined
}
// Equivalent recipes.
func (r1 *rule) equivRecipe(r2 *rule) bool {
if r1.recipe != r2.recipe {
return false
}
if len(r1.shell) != len(r2.shell) {
return false
}
for i := range r1.shell {
if r1.shell[i] != r2.shell[i] {
return false
}
}
return true
}
// A set of rules.
type ruleSet struct {
vars map[string][]string
rules []rule
// map a target to an array of indexes into rules
targetrules map[string][]int
}
// Read attributes for an array of strings, updating the rule.
func (r *rule) parseAttribs(inputs []string) *attribError {
for i := 0; i < len(inputs); i++ {
input := inputs[i]
pos := 0
for pos < len(input) {
c, w := utf8.DecodeRuneInString(input[pos:])
switch c {
case 'D':
r.attributes.delFailed = true
case 'E':
r.attributes.nonstop = true
case 'N':
r.attributes.forcedTimestamp = true
case 'n':
r.attributes.nonvirtual = true
case 'Q':
r.attributes.quiet = true
case 'R':
r.attributes.regex = true
case 'U':
r.attributes.update = true
case 'V':
r.attributes.virtual = true
case 'X':
r.attributes.exclusive = true
case 'P':
if pos+w < len(input) {
r.command = append(r.command, input[pos+w:])
}
r.command = append(r.command, inputs[i+1:]...)
return nil
case 'S':
if pos+w < len(input) {
r.shell = append(r.shell, input[pos+w:])
}
r.shell = append(r.shell, inputs[i+1:]...)
return nil
default:
return &attribError{c}
}
pos += w
}
}
return nil
}
// Add a rule to the rule set.
func (rs *ruleSet) add(r rule) {
rs.rules = append(rs.rules, r)
k := len(rs.rules) - 1
for i := range r.targets {
if r.targets[i].rpat == nil {
rs.targetrules[r.targets[i].spat] =
append(rs.targetrules[r.targets[i].spat], k)
}
}
}
func isValidVarName(v string) bool {
for i := 0; i < len(v); {
c, w := utf8.DecodeRuneInString(v[i:])
if i == 0 && !(isalpha(c) || c == '_') {
return false
} else if !(isalnum(c) || c == '_') {
return false
}
i += w
}
return true
}
func isdigit(c rune) bool {
return '0' <= c && c <= '9'
}
func isalpha(c rune) bool {
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
}
func isalnum(c rune) bool {
return isalpha(c) || isdigit(c)
}
type assignmentError struct {
what string
where token
}
// Parse and execute assignment operation.
func (rs *ruleSet) executeAssignment(ts []token) *assignmentError {
assignee := ts[0].val
if !isValidVarName(assignee) {
return &assignmentError{
fmt.Sprintf("target of assignment is not a valid variable name: \"%s\"", assignee),
ts[0]}
}
// interpret tokens in assignment context
input := make([]string, 0)
for i := 1; i < len(ts); i++ {
if ts[i].typ != tokenWord || (i > 1 && ts[i-1].typ != tokenWord) {
if len(input) == 0 {
input = append(input, ts[i].val)
} else {
input[len(input)-1] += ts[i].val
}
} else {
input = append(input, ts[i].val)
}
}
// expanded variables
vals := make([]string, 0)
for i := 0; i < len(input); i++ {
vals = append(vals, expand(input[i], rs.vars, true)...)
}
rs.vars[assignee] = vals
return nil
}