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
data/lib/rulebow/cli.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
module Rulebow
|
2
|
+
|
3
|
+
##
|
4
|
+
# Rulebow's command line interface.
|
5
|
+
#
|
6
|
+
class CLI
|
7
|
+
|
8
|
+
# Fire her up!
|
9
|
+
def self.fire!(argv=ARGV)
|
10
|
+
new(argv).fire!
|
11
|
+
end
|
12
|
+
|
13
|
+
# Initialize new instance of Rulebow::CLI.
|
14
|
+
# If `argv` is not provided than ARGV is used.
|
15
|
+
#
|
16
|
+
# argv - Command line argument. [Array<String>]
|
17
|
+
#
|
18
|
+
# Returns nothing.
|
19
|
+
def initialize(argv=ARGV)
|
20
|
+
#begin
|
21
|
+
# require 'dotopts'
|
22
|
+
#rescue LoadError
|
23
|
+
#end
|
24
|
+
|
25
|
+
@argv = Array(argv || ARGV)
|
26
|
+
|
27
|
+
@script = nil
|
28
|
+
@watch = nil
|
29
|
+
@fresh = false
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns runner instance. [Runner]
|
33
|
+
def runner
|
34
|
+
@runner ||= (
|
35
|
+
Runner.new(
|
36
|
+
:script => @script,
|
37
|
+
:fresh => @fresh,
|
38
|
+
:watch => @watch
|
39
|
+
)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Execute command.
|
44
|
+
#
|
45
|
+
# command - Which command to execute.
|
46
|
+
#
|
47
|
+
# Returns nothing.
|
48
|
+
def fire!(argv=ARGV)
|
49
|
+
$DEBUG = argv.include?('--debug') || $DEBUG
|
50
|
+
return fire if $DEBUG
|
51
|
+
begin
|
52
|
+
fire
|
53
|
+
rescue => err
|
54
|
+
puts "#{$0}: error #{err}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fire her up!
|
59
|
+
def fire
|
60
|
+
args = cli_parse
|
61
|
+
|
62
|
+
ensure_options(args)
|
63
|
+
|
64
|
+
#if args.first == 'init' && !runner.root?
|
65
|
+
# init_project(*args)
|
66
|
+
#end
|
67
|
+
|
68
|
+
case @command
|
69
|
+
when :list
|
70
|
+
print_rules(*args)
|
71
|
+
when :help
|
72
|
+
print_help(*args)
|
73
|
+
else
|
74
|
+
runner.run(args.first)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parse command line arguments with just the prettiest
|
79
|
+
# little CLI parser there ever was.
|
80
|
+
def cli_parse
|
81
|
+
@command = nil
|
82
|
+
|
83
|
+
cli @argv,
|
84
|
+
"-R --rules" => lambda{ @command = :list },
|
85
|
+
"-H --help" => lambda{ @command = :help },
|
86
|
+
"-a --auto" => method(:watch=),
|
87
|
+
"-f --fresh" => method(:fresh!),
|
88
|
+
"-s --script" => method(:script=),
|
89
|
+
"-D --debug" => method(:debug!)
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
def print_help(*names)
|
94
|
+
puts "-R --rules list ruleset descriptions"
|
95
|
+
puts "-H --help list these help options"
|
96
|
+
puts "-a --auto [TIME] autorun every so many seconds"
|
97
|
+
puts "-f --fresh clear digest for fresh run"
|
98
|
+
puts "-s --script [SCRIPT] use alternate script"
|
99
|
+
puts "-D --debug extra error information"
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
def ensure_options(args)
|
104
|
+
erropts = args.select{ |a| a.start_with?('-') }
|
105
|
+
unless erropts.empty?
|
106
|
+
raise "unsupported options #{erropts.join(' ')}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Shall we make a fresh start of it, and remove all digests?
|
111
|
+
#
|
112
|
+
# Returns [Boolean]
|
113
|
+
def fresh?
|
114
|
+
@fresh
|
115
|
+
end
|
116
|
+
|
117
|
+
# Set fresh flag to true.
|
118
|
+
#
|
119
|
+
# Returns [Boolean]
|
120
|
+
def fresh!
|
121
|
+
@fresh = true
|
122
|
+
end
|
123
|
+
|
124
|
+
# Is debug mode on?
|
125
|
+
#
|
126
|
+
# Returns [Boolean]
|
127
|
+
def debug?
|
128
|
+
$DEBUG
|
129
|
+
end
|
130
|
+
|
131
|
+
# Set debug flag to true.
|
132
|
+
#
|
133
|
+
# Returns [Boolean]
|
134
|
+
def debug!
|
135
|
+
$DEBUG = true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Set the "watch" period --the rate at which
|
139
|
+
# autofiring of occurs.
|
140
|
+
#
|
141
|
+
# Returns [Fixnum[
|
142
|
+
def watch=(seconds)
|
143
|
+
@watch = seconds.to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
# Use alternate rulebow script.
|
147
|
+
#
|
148
|
+
# Returns [Array]
|
149
|
+
def script=(script)
|
150
|
+
@script = script.to_s
|
151
|
+
end
|
152
|
+
|
153
|
+
# Initialize project for rulebow.
|
154
|
+
#
|
155
|
+
# Returns nothing.
|
156
|
+
def init_project(*args)
|
157
|
+
# anything to do?
|
158
|
+
end
|
159
|
+
|
160
|
+
# Print out a list of availabe manual triggers.
|
161
|
+
#
|
162
|
+
# Returns nothing.
|
163
|
+
def print_rules(*names)
|
164
|
+
names = nil if names.empty?
|
165
|
+
puts "(#{runner.root})"
|
166
|
+
runner.rulesets.each do |name, set|
|
167
|
+
next unless names.member?(name.to_s) if names
|
168
|
+
print "#{name}"
|
169
|
+
print " (#{set.chain.join(' ')})" unless set.chain.empty?
|
170
|
+
puts
|
171
|
+
set.docs.each_with_index do |d, i|
|
172
|
+
puts " * #{d}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
#exit
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Kernel
|
2
|
+
private
|
3
|
+
#
|
4
|
+
# CLI is based on Clap library
|
5
|
+
# Copyright (c) 2010 Michel Martens
|
6
|
+
#
|
7
|
+
def cli(*args)
|
8
|
+
opts = (Hash === args.last ? args.pop : nil)
|
9
|
+
argv = args.first || ARGV.dup
|
10
|
+
args = []
|
11
|
+
|
12
|
+
#raise ArgumentError unless opts
|
13
|
+
|
14
|
+
# Split option aliases.
|
15
|
+
opts = opts.inject({}) do |h,(k,v)|
|
16
|
+
k.to_s.split(/\s+/).each{|o| h[o]=v}; h
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convert single dash flags into multiple flags.
|
20
|
+
argv = argv.inject([]) do |a, v|
|
21
|
+
if v[0,1] == '-' && v[1,1] != '-'
|
22
|
+
a.concat(v[1..-1].chars.map{|c| "-#{c}"})
|
23
|
+
else
|
24
|
+
a << v
|
25
|
+
end
|
26
|
+
a
|
27
|
+
end
|
28
|
+
|
29
|
+
while argv.any?
|
30
|
+
item = argv.shift
|
31
|
+
flag = opts[item]
|
32
|
+
|
33
|
+
if flag
|
34
|
+
# Work around lambda semantics in 1.8.7.
|
35
|
+
arity = [flag.arity, 0].max
|
36
|
+
|
37
|
+
# Raise if there are not enough parameters
|
38
|
+
# available for the flag.
|
39
|
+
if argv.size < arity
|
40
|
+
raise ArgumentError
|
41
|
+
end
|
42
|
+
|
43
|
+
# Call the lambda with N items from argv,
|
44
|
+
# where N is the lambda's arity.
|
45
|
+
flag.call(*argv.shift(arity))
|
46
|
+
else
|
47
|
+
|
48
|
+
# Collect the items that don't correspond to
|
49
|
+
# flags.
|
50
|
+
args << item
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
args
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# We are going to try doing it this way. If it proves a problem
|
2
|
+
# we'll make a special class.
|
3
|
+
#
|
4
|
+
class TrueClass
|
5
|
+
def empty?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def &(other)
|
10
|
+
other
|
11
|
+
end
|
12
|
+
|
13
|
+
def |(other)
|
14
|
+
other
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
class Array
|
20
|
+
alias and_without_t :&
|
21
|
+
alias or_without_t :|
|
22
|
+
|
23
|
+
def |(other)
|
24
|
+
TrueClass === other ? dup : or_without_t(other)
|
25
|
+
end
|
26
|
+
|
27
|
+
def &(other)
|
28
|
+
TrueClass === other ? dup : and_without_t(other)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
=begin
|
34
|
+
class TrueArray < Array
|
35
|
+
|
36
|
+
def each; end
|
37
|
+
|
38
|
+
def size
|
39
|
+
1 # ?
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def &(other)
|
47
|
+
other.dup
|
48
|
+
end
|
49
|
+
|
50
|
+
def |(other)
|
51
|
+
other.dup
|
52
|
+
end
|
53
|
+
|
54
|
+
## If this would have worked we would not have had
|
55
|
+
## to override Array.
|
56
|
+
#def coerce(other)
|
57
|
+
# return self, other
|
58
|
+
#end
|
59
|
+
end
|
60
|
+
=end
|
61
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
module Rulebow
|
2
|
+
|
3
|
+
##
|
4
|
+
# Digest class is used to read and write lists of files with their
|
5
|
+
# associated checksums. This class uses SHA1.
|
6
|
+
#
|
7
|
+
class Digest
|
8
|
+
|
9
|
+
# The name of the master digest.
|
10
|
+
DEFAULT_NAME = 'default'
|
11
|
+
|
12
|
+
# System instance. [System]
|
13
|
+
attr :system
|
14
|
+
|
15
|
+
# Digest of files as they are presently on disk. [Hash]
|
16
|
+
attr :current
|
17
|
+
|
18
|
+
# Digest of files as saved in the digest file. [Hash]
|
19
|
+
attr :saved
|
20
|
+
|
21
|
+
# Initialize new instance of Digest.
|
22
|
+
#
|
23
|
+
# Options
|
24
|
+
# ignore - Instance of Ignore for filtering unwanted files. [Ignore]
|
25
|
+
# mark - Name of digest to load. [String]
|
26
|
+
#
|
27
|
+
def initialize(system)
|
28
|
+
@system = system
|
29
|
+
#@name = (options[:name] || MASTER_NAME).to_s
|
30
|
+
#@ignore = options[:ignore]
|
31
|
+
|
32
|
+
@filename = system.state_file
|
33
|
+
|
34
|
+
@current = Hash.new{ |h,k| h[k.to_s] = {} }
|
35
|
+
@saved = Hash.new{ |h,k| h[k.to_s] = {} }
|
36
|
+
|
37
|
+
read
|
38
|
+
refresh
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get current digest for a given ruleset.
|
42
|
+
def [](ruleset)
|
43
|
+
for_ruleset(ruleset)
|
44
|
+
end
|
45
|
+
|
46
|
+
# The digest file's path.
|
47
|
+
#
|
48
|
+
# Returns [String]
|
49
|
+
def filename
|
50
|
+
@filename
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove all digests.
|
54
|
+
def clear_all
|
55
|
+
FileUtils.rm(filename)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Load digest from file system.
|
59
|
+
#
|
60
|
+
# Returns nothing.
|
61
|
+
def read
|
62
|
+
return unless File.exist?(filename)
|
63
|
+
|
64
|
+
name = DEFAULT_NAME
|
65
|
+
save = Hash.new{ |h,k| h[k.to_s] = {} }
|
66
|
+
|
67
|
+
File.read(filename).lines.each do |line|
|
68
|
+
if md = /^\[(\w+)\]$/.match(line)
|
69
|
+
name = md[1]
|
70
|
+
end
|
71
|
+
if md = /^(\w+)\s+(.*?)$/.match(line)
|
72
|
+
save[name][md[2]] = md[1]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
save.each do |name, digest|
|
77
|
+
@saved[name] = digest
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Refresh current digest for a given ruleset, or all rulesets if not given.
|
82
|
+
#
|
83
|
+
# Returns nothing.
|
84
|
+
def refresh(ruleset=nil)
|
85
|
+
if ruleset
|
86
|
+
ruleset = getruleset(ruleset)
|
87
|
+
current[ruleset.name.to_s] = ruleset.watchlist.digest
|
88
|
+
else
|
89
|
+
system.rulesets.each do |name, ruleset|
|
90
|
+
refresh(ruleset)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Save current digest.
|
96
|
+
#
|
97
|
+
# Returns nothing.
|
98
|
+
def save(ruleset=nil)
|
99
|
+
if ruleset
|
100
|
+
ruleset = getruleset(ruleset)
|
101
|
+
refresh(ruleset)
|
102
|
+
saved[ruleset.name.to_s] = current[ruleset.name.to_s]
|
103
|
+
else
|
104
|
+
refresh
|
105
|
+
saved = current
|
106
|
+
end
|
107
|
+
|
108
|
+
dir = File.dirname(filename)
|
109
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
110
|
+
File.open(filename, 'w') do |f|
|
111
|
+
f << to_s
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Remove digest.
|
116
|
+
def remove(ruleset)
|
117
|
+
case ruleset
|
118
|
+
when Ruleset
|
119
|
+
current.remove(ruleset.name)
|
120
|
+
else
|
121
|
+
current.remove(ruleset.to_str)
|
122
|
+
end
|
123
|
+
save
|
124
|
+
end
|
125
|
+
|
126
|
+
# Produce the representation of the digest that is stored to disk.
|
127
|
+
#
|
128
|
+
# Returns digest file format. [String]
|
129
|
+
def to_s
|
130
|
+
s = ""
|
131
|
+
saved.each do |name, list|
|
132
|
+
s << "[#{name}]\n"
|
133
|
+
list.each do |path, id|
|
134
|
+
s << "#{id} #{path}\n"
|
135
|
+
end
|
136
|
+
s << "\n"
|
137
|
+
end
|
138
|
+
s
|
139
|
+
end
|
140
|
+
|
141
|
+
# Compute the sha1 identifer for a file.
|
142
|
+
#
|
143
|
+
# file - path to a file
|
144
|
+
#
|
145
|
+
# Returns [String] SHA1 digest string.
|
146
|
+
def checksum(file)
|
147
|
+
sha = ::Digest::SHA1.new
|
148
|
+
File.open(file, 'r') do |fh|
|
149
|
+
fh.each_line do |l|
|
150
|
+
sha << l
|
151
|
+
end
|
152
|
+
end
|
153
|
+
sha.hexdigest
|
154
|
+
end
|
155
|
+
|
156
|
+
# Filter files of those to be ignored.
|
157
|
+
#
|
158
|
+
# ruleset - instance of {Ruleset}
|
159
|
+
# files - files to be filtered
|
160
|
+
#
|
161
|
+
# Return [Array<String>]
|
162
|
+
def filter(ruleset, files)
|
163
|
+
#case ruleset.ignore
|
164
|
+
#when Watchlist
|
165
|
+
ruleset.digest.filter(files)
|
166
|
+
#when Array
|
167
|
+
# files.reject!{ |path| ignore.any?{ |ig| /^#{ig}/ =~ path } }
|
168
|
+
#else
|
169
|
+
# files
|
170
|
+
#end
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
def for_ruleset(ruleset)
|
175
|
+
For.instance(self, getruleset(ruleset))
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
#
|
181
|
+
def getruleset(ruleset)
|
182
|
+
case ruleset
|
183
|
+
when Ruleset
|
184
|
+
ruleset
|
185
|
+
else
|
186
|
+
system.rulesets[ruleset.to_sym]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
#
|
192
|
+
class For
|
193
|
+
|
194
|
+
#
|
195
|
+
def self.instance(digest, ruleset)
|
196
|
+
@instance ||= {}
|
197
|
+
@instance[[digest, ruleset]] ||= new(digest, ruleset)
|
198
|
+
end
|
199
|
+
|
200
|
+
def initialize(digest, ruleset)
|
201
|
+
@digest = digest
|
202
|
+
@ruleset = ruleset
|
203
|
+
end
|
204
|
+
|
205
|
+
attr :digest
|
206
|
+
|
207
|
+
attr :ruleset
|
208
|
+
|
209
|
+
def name
|
210
|
+
ruleset.name.to_s
|
211
|
+
end
|
212
|
+
|
213
|
+
def current
|
214
|
+
digest.current[name]
|
215
|
+
end
|
216
|
+
|
217
|
+
def saved
|
218
|
+
digest.saved[name]
|
219
|
+
end
|
220
|
+
|
221
|
+
# Filter files.
|
222
|
+
#
|
223
|
+
# Return [Array<String>]
|
224
|
+
def filter(files)
|
225
|
+
ruleset.watchlist.filter(files)
|
226
|
+
#case ruleset.ignore
|
227
|
+
#when Ignore
|
228
|
+
# ruleset.ignore.filter(list)
|
229
|
+
#when Array
|
230
|
+
# list.reject!{ |path| ignore.any?{ |ig| /^#{ig}/ =~ path } }
|
231
|
+
#else
|
232
|
+
# list
|
233
|
+
#end
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|