travis-conditions 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/Gemfile.lock +1 -1
- data/NOTES.md +107 -0
- data/README.md +261 -13
- data/bin/travis-conditions +34 -0
- data/lib/travis/conditions.rb +10 -16
- data/lib/travis/conditions/v0.rb +30 -0
- data/lib/travis/conditions/v0/data.rb +57 -0
- data/lib/travis/conditions/v0/eval.rb +70 -0
- data/lib/travis/conditions/v0/parser.rb +204 -0
- data/lib/travis/conditions/v1.rb +19 -0
- data/lib/travis/conditions/v1/boolean.rb +71 -0
- data/lib/travis/conditions/v1/data.rb +75 -0
- data/lib/travis/conditions/v1/eval.rb +114 -0
- data/lib/travis/conditions/v1/helper.rb +30 -0
- data/lib/travis/conditions/v1/parser.rb +214 -0
- data/lib/travis/conditions/version.rb +1 -1
- data/spec/conditions_spec.rb +15 -0
- data/spec/v0/conditions_spec.rb +15 -0
- data/spec/{data_spec.rb → v0/data_spec.rb} +6 -1
- data/spec/{eval_spec.rb → v0/eval_spec.rb} +1 -1
- data/spec/v0/fixtures/failures.txt +342 -0
- data/spec/v0/fixtures/passes.txt +1685 -0
- data/spec/{parser_spec.rb → v0/parser_spec.rb} +1 -1
- data/spec/v1/conditions_spec.rb +44 -0
- data/spec/v1/data_spec.rb +30 -0
- data/spec/v1/eval_spec.rb +349 -0
- data/spec/v1/fixtures/failures.txt +336 -0
- data/spec/v1/fixtures/passes.txt +1634 -0
- data/spec/v1/parser/boolean_spec.rb +215 -0
- data/spec/v1/parser/call_spec.rb +68 -0
- data/spec/v1/parser/comma_spec.rb +28 -0
- data/spec/v1/parser/cont_spec.rb +41 -0
- data/spec/v1/parser/eq_spec.rb +16 -0
- data/spec/v1/parser/in_list_spec.rb +60 -0
- data/spec/v1/parser/is_pred_spec.rb +24 -0
- data/spec/v1/parser/list_spec.rb +36 -0
- data/spec/v1/parser/operand_spec.rb +16 -0
- data/spec/v1/parser/parens_spec.rb +28 -0
- data/spec/v1/parser/quoted_spec.rb +24 -0
- data/spec/v1/parser/re_spec.rb +16 -0
- data/spec/v1/parser/regex_spec.rb +12 -0
- data/spec/v1/parser/space_spec.rb +40 -0
- data/spec/v1/parser/term_spec.rb +84 -0
- data/spec/v1/parser/val_spec.rb +24 -0
- data/spec/v1/parser/var_spec.rb +16 -0
- data/spec/v1/parser_spec.rb +486 -0
- data/spec/v1/user_spec.rb +223 -0
- metadata +48 -9
- data/lib/travis/conditions/data.rb +0 -45
- data/lib/travis/conditions/eval.rb +0 -68
- data/lib/travis/conditions/parser.rb +0 -202
@@ -0,0 +1,71 @@
|
|
1
|
+
# https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
|
2
|
+
#
|
3
|
+
# term = 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
|
4
|
+
#
|
5
|
+
# not = 'not' | 'NOT';
|
6
|
+
# and = 'and' | 'AND' | '&&';
|
7
|
+
# or = 'or' | 'OR' | '||';
|
8
|
+
#
|
9
|
+
# expr = expr or expr
|
10
|
+
# | expr and expr
|
11
|
+
# | not expr
|
12
|
+
# | '(' expr ')'
|
13
|
+
# | var
|
14
|
+
|
15
|
+
# E --> T {( "or" ) T}
|
16
|
+
# T --> P {( "and" ) P}
|
17
|
+
# P --> v | "(" E ")" | "NOT" E
|
18
|
+
|
19
|
+
# expr = exp2 { or exp2 }
|
20
|
+
# exp2 = oprd { and oprd }
|
21
|
+
# oprd = term | '(' expr ')' | not expr
|
22
|
+
|
23
|
+
module Travis
|
24
|
+
module Conditions
|
25
|
+
module V1
|
26
|
+
module Boolean
|
27
|
+
AND = /(and|&&)/i
|
28
|
+
OR = /(or|\|\|)/i
|
29
|
+
NOT = /(not|!)/i
|
30
|
+
|
31
|
+
BOP = {
|
32
|
+
'and' => :and,
|
33
|
+
'&&' => :and,
|
34
|
+
'or' => :or,
|
35
|
+
'||' => :and,
|
36
|
+
'not' => :not
|
37
|
+
}
|
38
|
+
|
39
|
+
def expr
|
40
|
+
lft = expr_
|
41
|
+
lft = [:or, lft, expr_] while op(OR)
|
42
|
+
lft
|
43
|
+
end
|
44
|
+
|
45
|
+
def expr_
|
46
|
+
lft = oprd
|
47
|
+
lft = [:and, lft, oprd] while op(AND)
|
48
|
+
lft
|
49
|
+
end
|
50
|
+
|
51
|
+
def oprd
|
52
|
+
t = parens { expr } and return t
|
53
|
+
t = not_ { oprd } and return t
|
54
|
+
term
|
55
|
+
end
|
56
|
+
|
57
|
+
def not_
|
58
|
+
pos = self.pos
|
59
|
+
space { scan(NOT) } or return
|
60
|
+
t = yield and return [:not, t]
|
61
|
+
str.pos = pos
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def op(op)
|
66
|
+
op = space { scan(op) } and BOP[op.downcase]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Travis
|
2
|
+
module Conditions
|
3
|
+
module V1
|
4
|
+
class Data < Struct.new(:data)
|
5
|
+
def initialize(data)
|
6
|
+
super(normalize(data))
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
data[key.to_sym]
|
11
|
+
end
|
12
|
+
|
13
|
+
def env(key)
|
14
|
+
data.fetch(:env, {})[key.to_sym]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def symbolize(obj)
|
20
|
+
case obj
|
21
|
+
when Hash
|
22
|
+
obj.map { |key, value| [key&.to_sym, symbolize(value)] }.to_h
|
23
|
+
when Array
|
24
|
+
obj.map { |obj| symbolize(obj) }
|
25
|
+
else
|
26
|
+
obj
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def cast(obj)
|
31
|
+
case obj.to_s.downcase
|
32
|
+
when 'false'
|
33
|
+
false
|
34
|
+
when 'true'
|
35
|
+
true
|
36
|
+
else
|
37
|
+
obj
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def normalize(data)
|
42
|
+
data = symbolize(data)
|
43
|
+
data[:env] = normalize_env(data[:env])
|
44
|
+
data
|
45
|
+
end
|
46
|
+
|
47
|
+
def normalize_env(env)
|
48
|
+
symbolize(to_h(env || {}))
|
49
|
+
rescue TypeError
|
50
|
+
raise arg_error(env)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_h(obj)
|
54
|
+
case obj
|
55
|
+
when Hash
|
56
|
+
obj
|
57
|
+
else
|
58
|
+
Array(obj).map { |obj| split(obj.to_s) }.to_h
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def split(str)
|
63
|
+
str = str.strip
|
64
|
+
raise arg_error(str) if str.empty? || !str.include?("=")
|
65
|
+
lft, rgt = str.split('=', 2)
|
66
|
+
[lft, cast(rgt)]
|
67
|
+
end
|
68
|
+
|
69
|
+
def arg_error(arg)
|
70
|
+
ArgumentError.new("Invalid env data (#{arg.inspect} given)")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Travis
|
2
|
+
module Conditions
|
3
|
+
module V1
|
4
|
+
class Eval < Struct.new(:sexp, :data)
|
5
|
+
def apply
|
6
|
+
!!evl(sexp)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def evl(expr)
|
12
|
+
expr = send(*expr) if expr.is_a?(Array)
|
13
|
+
cast(expr)
|
14
|
+
end
|
15
|
+
|
16
|
+
def not(lft)
|
17
|
+
!evl(lft)
|
18
|
+
end
|
19
|
+
|
20
|
+
def or(lft, rgt)
|
21
|
+
evl(lft) || evl(rgt)
|
22
|
+
end
|
23
|
+
|
24
|
+
def and(lft, rgt)
|
25
|
+
evl(lft) && evl(rgt)
|
26
|
+
end
|
27
|
+
|
28
|
+
def eq(lft, rgt)
|
29
|
+
evl(lft) == evl(rgt)
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_eq(lft, rgt)
|
33
|
+
not eq(lft, rgt)
|
34
|
+
end
|
35
|
+
|
36
|
+
def match(lft, rgt)
|
37
|
+
evl(lft) =~ evl(rgt)
|
38
|
+
end
|
39
|
+
|
40
|
+
def not_match(lft, rgt)
|
41
|
+
not match(lft, rgt)
|
42
|
+
end
|
43
|
+
|
44
|
+
def in(lft, rgt)
|
45
|
+
rgt = rgt.map { |rgt| evl(rgt) }
|
46
|
+
rgt.include?(evl(lft))
|
47
|
+
end
|
48
|
+
|
49
|
+
def not_in(lft, rgt)
|
50
|
+
not evl([:in, lft, rgt])
|
51
|
+
end
|
52
|
+
|
53
|
+
def is(lft, rgt)
|
54
|
+
send(rgt, evl(lft))
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_not(lft, rgt)
|
58
|
+
not evl([:is, lft, rgt])
|
59
|
+
end
|
60
|
+
|
61
|
+
def val(str)
|
62
|
+
str
|
63
|
+
end
|
64
|
+
|
65
|
+
def reg(expr)
|
66
|
+
Regexp.new(evl(expr))
|
67
|
+
end
|
68
|
+
|
69
|
+
def var(name)
|
70
|
+
data[name]
|
71
|
+
end
|
72
|
+
|
73
|
+
def call(name, args)
|
74
|
+
send(name, *args.map { |arg| evl(arg) })
|
75
|
+
end
|
76
|
+
|
77
|
+
def env(key)
|
78
|
+
data.env(key)
|
79
|
+
end
|
80
|
+
|
81
|
+
def concat(*args)
|
82
|
+
args.inject('') { |str, arg| str + arg.to_s }
|
83
|
+
end
|
84
|
+
|
85
|
+
def present(value)
|
86
|
+
value.respond_to?(:empty?) && !value.empty?
|
87
|
+
end
|
88
|
+
|
89
|
+
def blank(value)
|
90
|
+
!present(value)
|
91
|
+
end
|
92
|
+
|
93
|
+
def true(value)
|
94
|
+
!!value
|
95
|
+
end
|
96
|
+
|
97
|
+
def false(value)
|
98
|
+
!value
|
99
|
+
end
|
100
|
+
|
101
|
+
def cast(obj)
|
102
|
+
case obj.to_s.downcase
|
103
|
+
when 'false'
|
104
|
+
false
|
105
|
+
when 'true'
|
106
|
+
true
|
107
|
+
else
|
108
|
+
obj
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Travis
|
2
|
+
module Conditions
|
3
|
+
module V1
|
4
|
+
module Helper
|
5
|
+
QUOTE = /["']{1}/
|
6
|
+
OPEN = /\(/
|
7
|
+
CLOSE = /\)/
|
8
|
+
SPACE = /\s*/
|
9
|
+
|
10
|
+
def quoted
|
11
|
+
return unless quote = scan(QUOTE)
|
12
|
+
scan(/[^#{quote}]*/).tap { scan(/#{quote}/) || err(quote) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def parens
|
16
|
+
space { skip(OPEN) } and space { yield }.tap { skip(CLOSE) || err(')') }
|
17
|
+
end
|
18
|
+
|
19
|
+
def space
|
20
|
+
skip(SPACE)
|
21
|
+
yield.tap { skip(SPACE) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def err(char)
|
25
|
+
raise ParseError, "expected #{char} at position #{pos} in: #{string.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'travis/conditions/v1/boolean'
|
4
|
+
require 'travis/conditions/v1/helper'
|
5
|
+
|
6
|
+
# var = 'type'
|
7
|
+
# | 'repo'
|
8
|
+
# | 'head_repo'
|
9
|
+
# | 'os'
|
10
|
+
# | 'dist'
|
11
|
+
# | 'group'
|
12
|
+
# | 'sudo'
|
13
|
+
# | 'language'
|
14
|
+
# | 'sender'
|
15
|
+
# | 'fork'
|
16
|
+
# | 'branch'
|
17
|
+
# | 'head_branch'
|
18
|
+
# | 'tag'
|
19
|
+
# | 'commit_message';
|
20
|
+
#
|
21
|
+
# func = 'env' || 'concat';
|
22
|
+
# pred = 'present' | 'blank';
|
23
|
+
#
|
24
|
+
# eq = '=' | '==' | '!=';
|
25
|
+
# re = '=~' | '~=` | '!~';
|
26
|
+
# in = 'in' | 'not in' | 'IN' | 'NOT IN';
|
27
|
+
# is = 'is' | 'is not' | 'IS' | 'IS NOT';
|
28
|
+
# or = 'or' | 'OR' | '||';
|
29
|
+
# and = 'and' | 'AND' | '&&';
|
30
|
+
#
|
31
|
+
# list = oprd | oprd ',' list;
|
32
|
+
# call = func '(' list ')';
|
33
|
+
# val = word | quoted;
|
34
|
+
#
|
35
|
+
# oprd = var | val | call;
|
36
|
+
#
|
37
|
+
# term = oprd is pred
|
38
|
+
# | oprd in '(' list ')'
|
39
|
+
# | oprd re regx
|
40
|
+
# | oprd eq oprd
|
41
|
+
# | oprd;
|
42
|
+
#
|
43
|
+
# expr = expr or expr
|
44
|
+
# | expr and expr
|
45
|
+
# | not expr
|
46
|
+
# | '(' expr ')'
|
47
|
+
# | term
|
48
|
+
|
49
|
+
module Travis
|
50
|
+
module Conditions
|
51
|
+
module V1
|
52
|
+
class Parser
|
53
|
+
extend Forwardable
|
54
|
+
include Boolean, Helper
|
55
|
+
|
56
|
+
VAR = /type|repo|head_repo|os|dist|group|sudo|language|sender|fork|
|
57
|
+
branch|head_branch|tag|commit_message/ix
|
58
|
+
|
59
|
+
PRED = /present|blank|true|false/i
|
60
|
+
FUNC = /env|concat/i
|
61
|
+
IN = /in|not in/i
|
62
|
+
IS = /is not|is/i
|
63
|
+
EQ = /==|=/
|
64
|
+
NEQ = /!=/
|
65
|
+
RE = /=~|~=/
|
66
|
+
NRE = /!~/
|
67
|
+
COMMA = /,/
|
68
|
+
WORD = /[^\s\(\)"',=!]+/
|
69
|
+
REGEX = %r(/.+/|\S*[^\s\)]+)
|
70
|
+
CONT = /\\\s*[\n\r]/
|
71
|
+
|
72
|
+
OP = {
|
73
|
+
'=' => :eq,
|
74
|
+
'==' => :eq,
|
75
|
+
'!=' => :not_eq,
|
76
|
+
'=~' => :match,
|
77
|
+
'~=' => :match,
|
78
|
+
'!~' => :not_match
|
79
|
+
}
|
80
|
+
|
81
|
+
MSGS = {
|
82
|
+
parse_error: 'Could not parse %s',
|
83
|
+
shell_var: 'Variable names cannot start with a dollar (shell code does not work). If you are trying to compare to an env var, please use env("name")',
|
84
|
+
shell_str: 'Strings cannot start with a dollar (shell code does not work). This can be bypassed by quoting the string.'
|
85
|
+
}
|
86
|
+
|
87
|
+
def_delegators :str, :scan, :skip, :string, :pos, :peek
|
88
|
+
attr_reader :str
|
89
|
+
|
90
|
+
def initialize(str)
|
91
|
+
@str = StringScanner.new(filter(str))
|
92
|
+
end
|
93
|
+
|
94
|
+
def filter(str)
|
95
|
+
str.gsub(CONT, ' ')
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse
|
99
|
+
res = expr
|
100
|
+
error(:parse_error, string.inspect) unless res && !str.rest?
|
101
|
+
res
|
102
|
+
end
|
103
|
+
|
104
|
+
def term
|
105
|
+
lft = operand
|
106
|
+
lst = in_list(lft) and return lst
|
107
|
+
prd = is_pred(lft) and return prd
|
108
|
+
op = re and return [op, lft, regex]
|
109
|
+
op = eq and return [op, lft, operand]
|
110
|
+
lft
|
111
|
+
end
|
112
|
+
|
113
|
+
def operand
|
114
|
+
op = space { var || call || val }
|
115
|
+
op or err('an operand')
|
116
|
+
end
|
117
|
+
|
118
|
+
def regex
|
119
|
+
val = call
|
120
|
+
return [:reg, val] if val
|
121
|
+
reg = space { scan(REGEX) }
|
122
|
+
[:reg, reg.gsub(/^\/|\/$/, '')] or err('an operand')
|
123
|
+
end
|
124
|
+
|
125
|
+
def eq
|
126
|
+
op = space { scan(EQ) || scan(NEQ) } and OP[op]
|
127
|
+
end
|
128
|
+
|
129
|
+
def re
|
130
|
+
op = space { scan(RE) || scan(NRE) } and OP[op]
|
131
|
+
end
|
132
|
+
|
133
|
+
def var
|
134
|
+
pos = str.pos
|
135
|
+
var = scan(VAR)
|
136
|
+
error(:shell_var) if var && var[0] == '$'
|
137
|
+
return [:var, var.downcase.to_sym] if var && boundary?
|
138
|
+
str.pos = pos
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
142
|
+
BOUND = /[\s,=)|]/
|
143
|
+
|
144
|
+
def boundary?
|
145
|
+
peek(1) =~ BOUND || str.eos?
|
146
|
+
end
|
147
|
+
|
148
|
+
def call
|
149
|
+
return unless name = func
|
150
|
+
args = parens { list }
|
151
|
+
args or return
|
152
|
+
return [:call, name.to_sym, args]
|
153
|
+
end
|
154
|
+
|
155
|
+
def val
|
156
|
+
val = quoted || word and [:val, val]
|
157
|
+
end
|
158
|
+
|
159
|
+
def is_pred(term)
|
160
|
+
op = is or return
|
161
|
+
pr = pred or return
|
162
|
+
[op.downcase.sub(' ', '_').to_sym, term, pr.downcase.to_sym]
|
163
|
+
end
|
164
|
+
|
165
|
+
def in_list(term)
|
166
|
+
op = self.in or return
|
167
|
+
list = parens { list! }
|
168
|
+
[op.downcase.sub(' ', '_').to_sym, term, list]
|
169
|
+
end
|
170
|
+
|
171
|
+
def list!
|
172
|
+
list.tap { |list| err 'a list of values' if list.empty? }
|
173
|
+
end
|
174
|
+
|
175
|
+
def list
|
176
|
+
return [] unless item = var || call || val
|
177
|
+
list = comma ? [item] + self.list : [item]
|
178
|
+
skip(COMMA)
|
179
|
+
list.compact
|
180
|
+
end
|
181
|
+
|
182
|
+
def func
|
183
|
+
space { scan(FUNC) }
|
184
|
+
end
|
185
|
+
|
186
|
+
def pred
|
187
|
+
space { scan(PRED)&.to_sym }
|
188
|
+
end
|
189
|
+
|
190
|
+
def word
|
191
|
+
str = space { scan(WORD) }
|
192
|
+
error(:shell_str) if str && str[0] == '$'
|
193
|
+
str
|
194
|
+
end
|
195
|
+
|
196
|
+
def in
|
197
|
+
space { scan(IN) }
|
198
|
+
end
|
199
|
+
|
200
|
+
def is
|
201
|
+
space { scan(IS) }
|
202
|
+
end
|
203
|
+
|
204
|
+
def comma
|
205
|
+
space { scan(COMMA) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def error(key, *vals)
|
209
|
+
raise ParseError, MSGS[key] % vals
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|