rulebow 0.4.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 +7 -0
- data/.index +61 -0
- data/.yardopts +10 -0
- data/HISTORY.md +62 -0
- data/LICENSE.txt +25 -0
- data/README.md +168 -0
- data/bin/bow +4 -0
- data/demo/03_runner/01_applying_rules.md +51 -0
- data/demo/applique/ae.rb +1 -0
- data/demo/applique/rulebow.rb +11 -0
- data/demo/overview.md +0 -0
- data/lib/rulebow.rb +21 -0
- data/lib/rulebow.yml +61 -0
- data/lib/rulebow/cli.rb +181 -0
- data/lib/rulebow/core_ext.rb +3 -0
- data/lib/rulebow/core_ext/boolean.rb +10 -0
- data/lib/rulebow/core_ext/cli.rb +56 -0
- data/lib/rulebow/core_ext/true_class.rb +61 -0
- data/lib/rulebow/digest.rb +240 -0
- data/lib/rulebow/fact.rb +118 -0
- data/lib/rulebow/ignore.rb +136 -0
- data/lib/rulebow/match.rb +26 -0
- data/lib/rulebow/rule.rb +63 -0
- data/lib/rulebow/ruleset.rb +308 -0
- data/lib/rulebow/runner.rb +445 -0
- data/lib/rulebow/shellutils.rb +84 -0
- data/lib/rulebow/system.rb +153 -0
- data/lib/rulebow/watchlist.rb +203 -0
- data/man/.gitignore +2 -0
- data/man/ergo.1.ronn +50 -0
- metadata +151 -0
@@ -0,0 +1,445 @@
|
|
1
|
+
module Rulebow
|
2
|
+
|
3
|
+
# Runner is the main class which controls execution.
|
4
|
+
#
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
RULEBOOK_GLOB = "{,.,_}{R,r}ulebook{,.rb}"
|
8
|
+
|
9
|
+
# Initialize new Runner instance.
|
10
|
+
#
|
11
|
+
# Returns nothing.
|
12
|
+
def initialize(options={})
|
13
|
+
self.ignore = options[:ignore]
|
14
|
+
|
15
|
+
self.trial = options[:trial]
|
16
|
+
self.fresh = options[:fresh]
|
17
|
+
self.watch = options[:watch]
|
18
|
+
|
19
|
+
if options[:system]
|
20
|
+
@system = options[:system]
|
21
|
+
@root = @system.root
|
22
|
+
else
|
23
|
+
locate_root
|
24
|
+
@system = System.new(:root=>root)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Project's root directory.
|
29
|
+
#
|
30
|
+
# Returns [String]
|
31
|
+
def root
|
32
|
+
@root
|
33
|
+
end
|
34
|
+
|
35
|
+
# Locate project root. This method ascends up the file system starting
|
36
|
+
# as the current working directory looking for a `Rulebook` file.
|
37
|
+
# When found, the directory in which it is found is returned as the root.
|
38
|
+
def locate_root
|
39
|
+
d = Dir.pwd
|
40
|
+
while d != home && d != '/'
|
41
|
+
f = Dir.glob(File.join(d, RULEBOOK_GLOB)).first
|
42
|
+
if f
|
43
|
+
@root = d
|
44
|
+
break
|
45
|
+
end
|
46
|
+
d = File.dirname(d)
|
47
|
+
end
|
48
|
+
raise(RootError, "cannot locate project root") unless @root
|
49
|
+
@root
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
#def root=(path)
|
54
|
+
# @root = path
|
55
|
+
#end
|
56
|
+
|
57
|
+
# Home directory.
|
58
|
+
#
|
59
|
+
# Returns [String]
|
60
|
+
def home
|
61
|
+
@home ||= File.expand_path('~')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Config script.
|
65
|
+
#def config
|
66
|
+
# @config ||= Dir[CONFIG_SCRIPT].first
|
67
|
+
#end
|
68
|
+
|
69
|
+
# Set config script.
|
70
|
+
#def config=(script)
|
71
|
+
# @config = script
|
72
|
+
#end
|
73
|
+
|
74
|
+
# Watch period, default is every 5 minutes.
|
75
|
+
#
|
76
|
+
# Returns [Fixnum]
|
77
|
+
def watch
|
78
|
+
@watch
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set watch seconds. Minimum watch time is 1 second.
|
82
|
+
# Setting watch before calling #run creates a simple loop.
|
83
|
+
# It can eat up CPU cycles so use it wisely. A watch time
|
84
|
+
# of 4 seconds is a good time period. If you are patient
|
85
|
+
# go for 15 seconds or more.
|
86
|
+
#
|
87
|
+
# Returns [Fixnum,nil]
|
88
|
+
def watch=(seconds)
|
89
|
+
if seconds
|
90
|
+
seconds = seconds.to_i
|
91
|
+
seconds = 1 if seconds < 1
|
92
|
+
@watch = seconds
|
93
|
+
else
|
94
|
+
@watch = nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Nullify digest and make a fresh run?
|
99
|
+
#
|
100
|
+
# Returns [Boolean]
|
101
|
+
def fresh?
|
102
|
+
@fresh
|
103
|
+
end
|
104
|
+
|
105
|
+
# Set whether to nullify digest and make a fresh run.
|
106
|
+
#
|
107
|
+
# Returns [Boolean]
|
108
|
+
def fresh=(boolean)
|
109
|
+
@fresh = !! boolean
|
110
|
+
end
|
111
|
+
|
112
|
+
# Is this trial-run only?
|
113
|
+
#
|
114
|
+
# TODO: Trial mode is not implemented yet!
|
115
|
+
#
|
116
|
+
# Returns [Boolean]
|
117
|
+
def trial?
|
118
|
+
@trial
|
119
|
+
end
|
120
|
+
|
121
|
+
# Set trial run mode.
|
122
|
+
#
|
123
|
+
# Arguments
|
124
|
+
# bool - Flag for trial mode. [Boolean]
|
125
|
+
#
|
126
|
+
# Returns `bool` flag. [Boolean]
|
127
|
+
def trial=(bool)
|
128
|
+
@trial = !!bool
|
129
|
+
end
|
130
|
+
|
131
|
+
# Set the root directory.
|
132
|
+
#
|
133
|
+
# Returns [String]
|
134
|
+
#def root=(dir)
|
135
|
+
# @root = dir if dir
|
136
|
+
#end
|
137
|
+
|
138
|
+
# Instance of {Rulebow::System}.
|
139
|
+
#
|
140
|
+
# Returns [System]
|
141
|
+
def system
|
142
|
+
@system #||= System.new(script)
|
143
|
+
end
|
144
|
+
|
145
|
+
## Rules script to load.
|
146
|
+
##
|
147
|
+
## Returns List of file paths. [Array]
|
148
|
+
#def script
|
149
|
+
# @script || (@system ? nil : Dir[RULES_SCRIPT].first)
|
150
|
+
#end
|
151
|
+
|
152
|
+
#
|
153
|
+
#
|
154
|
+
#
|
155
|
+
def rulesets
|
156
|
+
system.rulesets
|
157
|
+
end
|
158
|
+
|
159
|
+
# File globs to ignore.
|
160
|
+
#
|
161
|
+
# Returns [Ignore] instance.
|
162
|
+
#def digest
|
163
|
+
# @digest ||= Digest.new(:ignore=>ignore)
|
164
|
+
#end
|
165
|
+
|
166
|
+
# File globs to ignore.
|
167
|
+
#
|
168
|
+
# Returns [Ignore] instance.
|
169
|
+
def ignore
|
170
|
+
@ignore ||= []
|
171
|
+
end
|
172
|
+
|
173
|
+
# Set ignore.
|
174
|
+
def ignore=(list)
|
175
|
+
@ignore = list.to_a.flatten
|
176
|
+
end
|
177
|
+
|
178
|
+
# List of rules from the system.
|
179
|
+
#
|
180
|
+
# Returns [Array<Rule>]
|
181
|
+
def rules
|
182
|
+
system.rules
|
183
|
+
end
|
184
|
+
|
185
|
+
# Run rules.
|
186
|
+
#
|
187
|
+
# Returns nothing.
|
188
|
+
def run(name)
|
189
|
+
name = (name || :default).to_sym
|
190
|
+
|
191
|
+
if watch
|
192
|
+
autorun(name)
|
193
|
+
else
|
194
|
+
monorun(name)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
# Run rules once.
|
201
|
+
#
|
202
|
+
# Returns nothing.
|
203
|
+
def monorun(name)
|
204
|
+
Dir.chdir(root) do
|
205
|
+
fresh_digest(name) if fresh?
|
206
|
+
run_ruleset(name)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Run rules periodically.
|
211
|
+
#
|
212
|
+
# Returns nothing.
|
213
|
+
def autorun(name)
|
214
|
+
Dir.chdir(root) do
|
215
|
+
fresh_digest(name) if fresh?
|
216
|
+
|
217
|
+
trap("INT") { puts "\nBows down."; exit }
|
218
|
+
|
219
|
+
puts " ( RULEBOW "
|
220
|
+
puts " \\ (pid #{Process.pid})"
|
221
|
+
puts " ) "
|
222
|
+
puts " ##--------> "
|
223
|
+
puts " ) "
|
224
|
+
puts " / "
|
225
|
+
puts " ( "
|
226
|
+
|
227
|
+
loop do
|
228
|
+
run_ruleset(name)
|
229
|
+
sleep(watch)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Run a specific ruleruleset.
|
235
|
+
#
|
236
|
+
# name - Nmae of ruleset. [String].
|
237
|
+
#
|
238
|
+
# Returns nothing.
|
239
|
+
def run_ruleset(name)
|
240
|
+
rulesets = ruleset_chain(name)
|
241
|
+
rulesets.each do |ruleset|
|
242
|
+
run_rules(ruleset)
|
243
|
+
digest.save(ruleset)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Run all rulesets.
|
248
|
+
#
|
249
|
+
def run_all
|
250
|
+
run_rules(system)
|
251
|
+
digest.clear
|
252
|
+
digest.save
|
253
|
+
end
|
254
|
+
|
255
|
+
=begin
|
256
|
+
# Run only those rules with a specific rulesetmark.
|
257
|
+
#
|
258
|
+
# marks - Bookmark names. [Array<String>].
|
259
|
+
#
|
260
|
+
# Returns nothing.
|
261
|
+
def run_rulesetmarks(*marks)
|
262
|
+
system.rules.each do |rule|
|
263
|
+
case rule
|
264
|
+
when Ruleset
|
265
|
+
ruleset = rule
|
266
|
+
ruleset.rules.each do |rule|
|
267
|
+
next unless marks.any?{ |mark| rule.mark?(mark) }
|
268
|
+
rule.apply(latest_digest(rule))
|
269
|
+
end
|
270
|
+
else
|
271
|
+
next unless marks.any?{ |mark| rule.mark?(mark) }
|
272
|
+
rule.apply(latest_digest(rule))
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
save_digests(*marks)
|
277
|
+
end
|
278
|
+
=end
|
279
|
+
|
280
|
+
# Run set of rules.
|
281
|
+
#
|
282
|
+
# Returns nothing.
|
283
|
+
def run_rules(ruleset)
|
284
|
+
ruleset.rules.each do |rule|
|
285
|
+
rule.apply(digest[ruleset])
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Instance of Digest for this system.
|
290
|
+
def digest
|
291
|
+
@digest ||= Digest.new(system)
|
292
|
+
end
|
293
|
+
|
294
|
+
# Start with a clean slate by removing the digest.
|
295
|
+
#
|
296
|
+
# Returns nothing.
|
297
|
+
def fresh_digest(name)
|
298
|
+
if name
|
299
|
+
chain = ruleset_chain(name)
|
300
|
+
chain.each do |n|
|
301
|
+
digest.remove(n)
|
302
|
+
end
|
303
|
+
else
|
304
|
+
digest.clear_all
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Save digests for given rulesets.
|
309
|
+
#
|
310
|
+
# Returns nothing.
|
311
|
+
def save_digests(*rulesets)
|
312
|
+
rulesets.each do |name|
|
313
|
+
digest.save(name)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Get ruleset instance for a given command.
|
318
|
+
def ruleset_chain(name)
|
319
|
+
ruleset = verify_ruleset!(name)
|
320
|
+
chain = []
|
321
|
+
build_chain(ruleset, chain)
|
322
|
+
chain.uniq
|
323
|
+
end
|
324
|
+
|
325
|
+
#
|
326
|
+
def build_chain(ruleset, chain=[])
|
327
|
+
ruleset.chain.each do |name|
|
328
|
+
verify_ruleset!(name)
|
329
|
+
build_chain(rulesets[name], chain)
|
330
|
+
end
|
331
|
+
chain << ruleset
|
332
|
+
return chain
|
333
|
+
end
|
334
|
+
|
335
|
+
#
|
336
|
+
def verify_ruleset!(name)
|
337
|
+
name = name.to_sym
|
338
|
+
unless rulesets.key?(name)
|
339
|
+
raise(ArgumentError, "unknown ruleset name -- #{name}")
|
340
|
+
end
|
341
|
+
rulesets[name]
|
342
|
+
end
|
343
|
+
|
344
|
+
=begin
|
345
|
+
#
|
346
|
+
def calc_chain(*marks)
|
347
|
+
chain = []
|
348
|
+
order = []
|
349
|
+
|
350
|
+
marks.each do |mark|
|
351
|
+
if mark.include?(':')
|
352
|
+
k, p = mark.split(':')
|
353
|
+
raise "unknown chain -- #{k}" unless system.chains.key?(k)
|
354
|
+
order << system.chains[k][0..(system.chains[k].index(p))]
|
355
|
+
else
|
356
|
+
order << mark
|
357
|
+
end
|
358
|
+
end
|
359
|
+
order = order.flatten.uniq
|
360
|
+
|
361
|
+
order.each do |name|
|
362
|
+
complete_chain(name, chain)
|
363
|
+
end
|
364
|
+
|
365
|
+
return chain.uniq
|
366
|
+
end
|
367
|
+
|
368
|
+
#
|
369
|
+
def complete_chain(name, chain)
|
370
|
+
if system.rulesets.key?(name)
|
371
|
+
system.rulesets[name].chain.each do |n|
|
372
|
+
complete_chain(n, chain)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
chain << name
|
376
|
+
return chain
|
377
|
+
end
|
378
|
+
=end
|
379
|
+
|
380
|
+
# TODO: If we ever need this, we will need to put it in the state file.
|
381
|
+
#def save_pid
|
382
|
+
# File.open('.bow/pid', 'w') do |f|
|
383
|
+
# f << Process.pid.to_s
|
384
|
+
# end
|
385
|
+
#end
|
386
|
+
|
387
|
+
# Save file digest.
|
388
|
+
#
|
389
|
+
# Returns nothing.
|
390
|
+
#def save_digest(*marks)
|
391
|
+
# digest.save(*marks)
|
392
|
+
#end
|
393
|
+
|
394
|
+
# System runner.
|
395
|
+
#
|
396
|
+
# Returns [Runner]
|
397
|
+
#def runner
|
398
|
+
# Runner.new(system)
|
399
|
+
#end
|
400
|
+
|
401
|
+
# TODO: load configuration
|
402
|
+
#
|
403
|
+
#def rc?
|
404
|
+
# Dir.glob('{.c,c,C}onfig{.rb,}').first
|
405
|
+
#end
|
406
|
+
|
407
|
+
# Oh why is this still around? It's the original routine
|
408
|
+
# for running rules. It worked ass backward too. Checking
|
409
|
+
# facts and then applying rules that were attached to those
|
410
|
+
# facts.
|
411
|
+
#
|
412
|
+
#def run
|
413
|
+
# @facts.each do |fact|
|
414
|
+
# session = OpenStruct.new
|
415
|
+
# next unless fact.active?(info)
|
416
|
+
# @rules.each do |rule|
|
417
|
+
# if md = rule.match?(fact)
|
418
|
+
# if rule.arity == 0 or md == true
|
419
|
+
# rule.call(info)
|
420
|
+
# else
|
421
|
+
# rule.call(info,*md[1..-1])
|
422
|
+
# end
|
423
|
+
# end
|
424
|
+
# end
|
425
|
+
# end
|
426
|
+
#end
|
427
|
+
|
428
|
+
# TODO: support rc profiles
|
429
|
+
#if config = Rulebow.rc_config
|
430
|
+
# config.each do |c|
|
431
|
+
# if c.arity == 0
|
432
|
+
# system.instance_eval(&c)
|
433
|
+
# else
|
434
|
+
# c.call(system)
|
435
|
+
# end
|
436
|
+
# end
|
437
|
+
#end
|
438
|
+
|
439
|
+
#
|
440
|
+
class RootError < RuntimeError
|
441
|
+
end
|
442
|
+
|
443
|
+
end
|
444
|
+
|
445
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rulebow
|
2
|
+
|
3
|
+
# TODO: Borrow code from Detroit for ShellUtils and beef her up!
|
4
|
+
|
5
|
+
# File system utility methods.
|
6
|
+
#
|
7
|
+
module ShellUtils
|
8
|
+
# Shell out via system call.
|
9
|
+
#
|
10
|
+
# Arguments
|
11
|
+
# args - Argument vector. [Array]
|
12
|
+
#
|
13
|
+
# Returns success of shell invocation.
|
14
|
+
def sh(*args)
|
15
|
+
env = (Hash === args.last ? args.pop : {})
|
16
|
+
puts args.join(' ')
|
17
|
+
system(env, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Same as `#sh` but raises an error if shell fails.
|
21
|
+
def shell(*args)
|
22
|
+
success = sh(*args)
|
23
|
+
raise "shell failure: #{args.join(' ')}" unless success
|
24
|
+
end
|
25
|
+
alias :run :shell
|
26
|
+
|
27
|
+
#
|
28
|
+
def directory?(path)
|
29
|
+
File.directory?(path)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Synchronize a destination directory with a source directory.
|
33
|
+
#
|
34
|
+
# TODO: Augment FileUtils instead.
|
35
|
+
# TODO: Not every action needs to be verbose.
|
36
|
+
#
|
37
|
+
def sync(src, dst, options={})
|
38
|
+
src_files = Dir[File.join(src, '**', '*')].map{ |f| f.sub(src+'/', '') }
|
39
|
+
dst_files = Dir[File.join(dst, '**', '*')].map{ |f| f.sub(dst+'/', '') }
|
40
|
+
|
41
|
+
removal = dst_files - src_files
|
42
|
+
|
43
|
+
rm_dirs, rm_files = [], []
|
44
|
+
removal.each do |f|
|
45
|
+
path = File.join(dst, f)
|
46
|
+
if File.directory?(path)
|
47
|
+
rm_dirs << path
|
48
|
+
else
|
49
|
+
rm_files << path
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
rm_files.each { |f| rm(f) }
|
54
|
+
rm_dirs.each { |d| rmdir(d) }
|
55
|
+
|
56
|
+
src_files.each do |f|
|
57
|
+
src_path = File.join(src, f)
|
58
|
+
dst_path = File.join(dst, f)
|
59
|
+
if File.directory?(src_path)
|
60
|
+
mkdir_p(dst_path)
|
61
|
+
else
|
62
|
+
parent = File.dirname(dst_path)
|
63
|
+
mkdir_p(parent) unless File.directory?(parent)
|
64
|
+
install(src_path, dst_path)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# If FileUtils responds to a missing method, then call it.
|
70
|
+
#
|
71
|
+
def method_missing(s, *a, &b)
|
72
|
+
if FileUtils.respond_to?(s)
|
73
|
+
if $DRYRUN
|
74
|
+
FileUtils::DryRun.__send__(s, *a, &b)
|
75
|
+
else
|
76
|
+
FileUtils::Verbose.__send__(s, *a, &b)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
super(s, *a, &b)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|