log2mail 0.0.1.pre3 → 0.0.1.pre4
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/{INSTALL → INSTALL.md} +2 -0
- data/README.md +1 -1
- data/features/log2mail_configurations/config_1 +1 -1
- data/lib/ext/string.rb +6 -0
- data/lib/ext/symbol.rb +7 -0
- data/lib/log2mail.rb +1 -1
- data/lib/log2mail/config.rb +9 -210
- data/lib/log2mail/config/attribute.rb +21 -0
- data/lib/log2mail/config/config.rb +45 -0
- data/lib/log2mail/config/config_file_handler.rb +142 -0
- data/lib/log2mail/config/config_file_snippet.rb +17 -0
- data/lib/log2mail/config/old_config_file_handler.rb +220 -0
- data/lib/log2mail/config/parser.rb +144 -0
- data/lib/log2mail/config/section.rb +70 -0
- data/lib/log2mail/console/commands.rb +1 -1
- data/lib/log2mail/main.rb +2 -2
- data/lib/log2mail/version.rb +1 -1
- data/lib/log2mail/watcher.rb +1 -1
- data/log2mail.gemspec +1 -0
- data/man/log2mail.1 +1 -1
- data/man/log2mail.1.ronn +1 -1
- data/spec/factories.rb +126 -29
- data/spec/log2mail/{config_spec.rb → config/config_file_handler_spec.rb} +49 -3
- data/spec/log2mail/config/parser_spec.rb +467 -0
- data/spec/spec_helper.rb +1 -0
- metadata +29 -5
@@ -0,0 +1,220 @@
|
|
1
|
+
require_relative 'attribute'
|
2
|
+
require_relative 'section'
|
3
|
+
|
4
|
+
module Log2mail
|
5
|
+
module Config
|
6
|
+
class ConfigFileHandler
|
7
|
+
|
8
|
+
class <<self
|
9
|
+
def parse_config(config_path)
|
10
|
+
new(config_path)
|
11
|
+
# pp config.config
|
12
|
+
# config.files.each do |f|
|
13
|
+
# puts "File: #{f}"
|
14
|
+
# config.patterns_for_file(f).each do |pattern|
|
15
|
+
# puts " Pattern: #{pattern}; mailto: " + config.mailtos_for_pattern( f, pattern ).join(', ')
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :raw
|
22
|
+
|
23
|
+
def initialize(config_paths)
|
24
|
+
$logger.debug "Reading configuration from #{config_paths}"
|
25
|
+
@config_paths = Array(config_paths)
|
26
|
+
expand_paths
|
27
|
+
@raw = read_configuration
|
28
|
+
validate_configuration
|
29
|
+
end
|
30
|
+
|
31
|
+
# returns all the paths of all files needed to be watched
|
32
|
+
def files
|
33
|
+
@config.keys - [:defaults]
|
34
|
+
end
|
35
|
+
|
36
|
+
def file_patterns
|
37
|
+
h = {}
|
38
|
+
files.each do |file|
|
39
|
+
h[file] = patterns_for_file(file)
|
40
|
+
end
|
41
|
+
h
|
42
|
+
end
|
43
|
+
|
44
|
+
def patterns_for_file( file )
|
45
|
+
Hash(@config[file][:patterns]).keys + \
|
46
|
+
Hash(defaults[:patterns]).keys
|
47
|
+
end
|
48
|
+
|
49
|
+
def mailtos_for_pattern( file, pattern )
|
50
|
+
m = []
|
51
|
+
m.concat Hash( config_file_pattern(file, pattern)[:mailtos] ).keys
|
52
|
+
m.concat Hash(Hash(Hash(defaults[:patterns])[pattern])[:mailtos]).keys
|
53
|
+
m.concat Array(defaults[:mailtos]) if m.empty?
|
54
|
+
m.uniq
|
55
|
+
end
|
56
|
+
|
57
|
+
def settings_for_mailto( file, pattern, mailto )
|
58
|
+
h = defaults.reject {|k,v| k==:mailtos}
|
59
|
+
h.merge config_file_mailto(file, pattern, mailto)
|
60
|
+
end
|
61
|
+
|
62
|
+
def defaults
|
63
|
+
Hash(@config[:defaults])
|
64
|
+
end
|
65
|
+
|
66
|
+
def formatted( show_effective )
|
67
|
+
Terminal::Table.new do |t|
|
68
|
+
settings_header = show_effective ? 'Effective Settings' : 'Settings'
|
69
|
+
t << ['File', 'Pattern', 'Recipient', settings_header]
|
70
|
+
t << :separator
|
71
|
+
files.each do |file|
|
72
|
+
patterns_for_file(file).each do |pattern|
|
73
|
+
mailtos_for_pattern(file, pattern).each do |mailto|
|
74
|
+
settings = []
|
75
|
+
if show_effective
|
76
|
+
settings_for_mailto(file, pattern, mailto).each_pair \
|
77
|
+
{ |k,v| settings << '%s=%s' % [k,v] }
|
78
|
+
else
|
79
|
+
config_file_mailto(file, pattern, mailto).each_pair \
|
80
|
+
{ |k,v| settings << '%s=%s' % [k,v] }
|
81
|
+
end
|
82
|
+
t.add_row [file, pattern, mailto, settings.join($/)]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def config_file(file)
|
92
|
+
Hash(@config[file])
|
93
|
+
end
|
94
|
+
|
95
|
+
def config_file_pattern(file, pattern)
|
96
|
+
Hash( Hash( config_file(file)[:patterns] )[pattern] )
|
97
|
+
end
|
98
|
+
|
99
|
+
def config_file_mailtos(file, pattern)
|
100
|
+
Hash( config_file_pattern(file, pattern)[:mailtos] )
|
101
|
+
end
|
102
|
+
|
103
|
+
def config_file_mailto(file, pattern, mailto)
|
104
|
+
Hash( config_file_mailtos(file, pattern)[mailto] )
|
105
|
+
end
|
106
|
+
|
107
|
+
def expand_paths
|
108
|
+
expanded_paths = []
|
109
|
+
@config_paths.each do |path|
|
110
|
+
if ::File.directory?(path)
|
111
|
+
expanded_paths.concat Dir.glob( ::File.join( path, '*[^~#]' ) )
|
112
|
+
else
|
113
|
+
expanded_paths << path
|
114
|
+
end
|
115
|
+
end
|
116
|
+
@config_paths = expanded_paths
|
117
|
+
end
|
118
|
+
|
119
|
+
# tries to follow original code at https://github.com/lordlamer/log2mail/blob/master/config.cc#L192
|
120
|
+
def read_configuration
|
121
|
+
@config = {}
|
122
|
+
@config_paths.map do |file|
|
123
|
+
@section = nil; @pattern = nil; @mailto = nil
|
124
|
+
# section, pattern, mailto are reset for every file (but not when included by 'include')
|
125
|
+
parse_file( file )
|
126
|
+
end.join($/)
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_file( filename )
|
130
|
+
raw = ""
|
131
|
+
IO.readlines(filename).each_with_index do |line, lineno|
|
132
|
+
raw << line
|
133
|
+
parse(filename, line, lineno + 1)
|
134
|
+
end
|
135
|
+
raw
|
136
|
+
rescue Errno::ENOENT
|
137
|
+
fail Error, "Configuration file or directory not found (or not readable): #{filename}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse(file, line, lineno)
|
141
|
+
line.strip!
|
142
|
+
return if line =~ /^#/
|
143
|
+
return if line =~ /^$/
|
144
|
+
line =~ /^(\S+)\s*=?\s*"?(.*?)"?(\s*#.*)?$/ # drop double quotes on right hand side; drop comments
|
145
|
+
key, value = $1.to_sym, $2.strip
|
146
|
+
if key == :include # include shall work everywhere
|
147
|
+
parse_file( ::File.join(Pathname(file).parent, value) )
|
148
|
+
return
|
149
|
+
end
|
150
|
+
if key == :defaults and value.empty? # section: specifies top level; must be 'defaults' or 'file'
|
151
|
+
@section = key
|
152
|
+
@pattern = nil; @mailto = nil
|
153
|
+
fail Error, "Invalid section. Section 'defaults' already specified." if @config[@section]
|
154
|
+
@config[@section] = {}
|
155
|
+
elsif key == :file
|
156
|
+
@section = value
|
157
|
+
@pattern = nil; @mailto = nil
|
158
|
+
@config[@section] ||= {}
|
159
|
+
elsif key == :pattern # must come inside 'file' (or 'defaults')
|
160
|
+
# fail "Invalid section. All statements must appear after 'defaults' or 'file=...'" unless @section
|
161
|
+
@pattern = value; @mailto = nil
|
162
|
+
@config[@section][:patterns] ||= {}
|
163
|
+
warning { "Redefining pattern section '#{value}' which has been defined already for '#{@section}'." } \
|
164
|
+
if @config[@section][:patterns][value]
|
165
|
+
@config[@section][:patterns][value] = {}
|
166
|
+
elsif key == :mailto and @section != :defaults # must come inside 'pattern' (or 'defaults')
|
167
|
+
fail Error, "'mailto' statements only allowed inside 'pattern' or 'defaults'." unless @pattern
|
168
|
+
@mailto = value
|
169
|
+
@config[@section][:patterns][@pattern][:mailtos] ||= {}
|
170
|
+
warning { "Redefining mailto section '#{value}' which has been defined already for '#{@section}'." } \
|
171
|
+
if @config[@section][:patterns][@pattern][:mailtos][value]
|
172
|
+
@config[@section][:patterns][@pattern][:mailtos][value] = {}
|
173
|
+
else # everything else must come inside 'defaults' or 'mailto'
|
174
|
+
fail Error, "'#{key}' must be set within 'defaults' or 'mailto'." unless @section == :defaults or @mailto
|
175
|
+
if INT_OPTIONS.include?(key)
|
176
|
+
value = value.to_i
|
177
|
+
elsif STR_OPTIONS.include?(key)
|
178
|
+
elsif PATH_OPTIONS.include?(key)
|
179
|
+
value = ::File.expand_path( value, Pathname(file).parent ) unless Pathname(value).absolute?
|
180
|
+
elsif key == :mailto # special handling for 'mailto' in 'defaults' section
|
181
|
+
@config[:defaults][:mailtos] ||= []
|
182
|
+
@config[:defaults][:mailtos] << value
|
183
|
+
return # skip the 'mailto' entry itself
|
184
|
+
else
|
185
|
+
fail Error, "'#{key}' is an unknown configuration statement."
|
186
|
+
end
|
187
|
+
if @section == :defaults and !@pattern and !@mailto
|
188
|
+
warning { "Redefining value for '#{key}'." } if @config[@section][key]
|
189
|
+
@config[@section][key] = value
|
190
|
+
else
|
191
|
+
warning { "Redefining value for '#{key}'." } \
|
192
|
+
if @config[@section][:patterns][@pattern][:mailtos][@mailto][key]
|
193
|
+
@config[@section][:patterns][@pattern][:mailtos][@mailto][key] = value
|
194
|
+
end
|
195
|
+
end
|
196
|
+
rescue
|
197
|
+
fail Error, "#{file} (line #{lineno}): #{$!.message}#$/[#{$!.class} at #{$!.backtrace.first}]"
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate_configuration
|
201
|
+
files.each do |file|
|
202
|
+
patterns_for_file(file).each do |pattern|
|
203
|
+
mailtos = mailtos_for_pattern(file, pattern)
|
204
|
+
$logger.warn "Pattern #{file}:#{pattern} has no recipients." if mailtos.empty?
|
205
|
+
end
|
206
|
+
end
|
207
|
+
# FIXME: empty configuration should cause FATAL error
|
208
|
+
# TODO: illegal regexp pattern should cause ERROR
|
209
|
+
end
|
210
|
+
|
211
|
+
def warning(&block)
|
212
|
+
file = block.binding.eval('file')
|
213
|
+
lineno = block.binding.eval('lineno')
|
214
|
+
message = block.call
|
215
|
+
$logger.warn "#{file} (line #{lineno}): #{message}#$/[at #{caller.first}]"
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Log2mail
|
4
|
+
module Config
|
5
|
+
|
6
|
+
class ParseError < Log2mail::Error
|
7
|
+
def initialize( opts )
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
def original
|
11
|
+
@opts[:exception]
|
12
|
+
end
|
13
|
+
def to_s
|
14
|
+
'File \'' + @opts[:filename] + '\': ' + ($verbose ? original.cause.ascii_tree : original.to_s)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Transform < Parslet::Transform
|
19
|
+
|
20
|
+
rule(:quoted_string => simple(:value)) { value.to_s.gsub('\\"', '"') }
|
21
|
+
rule(:unquoted_string => simple(:value)) { value.to_s.strip }
|
22
|
+
rule(:integer => simple(:value)) { value.to_i }
|
23
|
+
|
24
|
+
rule(:attribute_name => simple(:key), :attribute_value => simple(:value)) do
|
25
|
+
Attribute.new( key, value )
|
26
|
+
end
|
27
|
+
|
28
|
+
rule(:section_name => simple(:key), :section_value => simple(:value)) do
|
29
|
+
Section.new( key, value )
|
30
|
+
end
|
31
|
+
rule(:section_name => simple(:key)) do
|
32
|
+
Section.new( key )
|
33
|
+
end
|
34
|
+
|
35
|
+
rule(:section => sequence(:values)) { |dict| merge_values( dict[:values] ) }
|
36
|
+
|
37
|
+
def self.merge_values( values )
|
38
|
+
values.reduce do |v1, v2|
|
39
|
+
fail "v1 must be a Section" unless v1.instance_of?(Section)
|
40
|
+
case v2
|
41
|
+
when Attribute
|
42
|
+
v1.attrs << v2
|
43
|
+
v1
|
44
|
+
when Section
|
45
|
+
v1.attrs << v2
|
46
|
+
v1
|
47
|
+
else
|
48
|
+
fail "Unsupported value class: #{v1.class}: #{v1.inspect}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
values.first
|
52
|
+
end
|
53
|
+
|
54
|
+
rule(:section => simple(:section)) do
|
55
|
+
fail "Unsupported section class: #{section.class}" unless section.instance_of?(Section)
|
56
|
+
section
|
57
|
+
end
|
58
|
+
|
59
|
+
rule(:config => sequence(:sections)) do
|
60
|
+
Config.new(sections)
|
61
|
+
end
|
62
|
+
|
63
|
+
rule(:config => simple(:comment)) { Config.new }
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
class Parser < Parslet::Parser
|
68
|
+
|
69
|
+
rule(:config) { block.repeat.as(:config) }
|
70
|
+
root(:config)
|
71
|
+
|
72
|
+
rule(:block) { (defaults_section | file_section).as(:section) | eol }
|
73
|
+
|
74
|
+
rule(:defaults_section) { defaults_section_head >> defaults_section_content }
|
75
|
+
rule(:file_section) { file_section_head >> file_section_content }
|
76
|
+
rule(:defaults_section_content) { ( mailto_section.as(:section) | pattern_section.as(:section) | attribute | eol ).repeat }
|
77
|
+
rule(:file_section_content) { ( pattern_section.as(:section) | attribute | eol ).repeat }
|
78
|
+
|
79
|
+
rule(:pattern_section) { pattern_section_head >> pattern_section_content }
|
80
|
+
rule(:pattern_section_content) { ( mailto_section.as(:section) | attribute | eol ).repeat }
|
81
|
+
|
82
|
+
rule(:mailto_section) { mailto_section_head >> mailto_section_content }
|
83
|
+
rule(:mailto_section_content) { attributes }
|
84
|
+
|
85
|
+
def line_expression( expr )
|
86
|
+
space? >> expr >> eol
|
87
|
+
end
|
88
|
+
|
89
|
+
def equation( name, value )
|
90
|
+
line_expression( name >> space? >> str('=') >> space? >> value )
|
91
|
+
end
|
92
|
+
|
93
|
+
rule(:defaults_section_head) { line_expression( str('defaults').as(:section_name) ) }
|
94
|
+
rule(:file_section_head) { equation( str('file').as(:section_name), value.as(:section_value)) }
|
95
|
+
rule(:mailto_section_head) { equation( str('mailto').as(:section_name), value.as(:section_value)) }
|
96
|
+
rule(:pattern_section_head) { equation( str('pattern').as(:section_name), value.as(:section_value)) }
|
97
|
+
|
98
|
+
rule(:attribute) { equation( valid_attr_name.as(:attribute_name), value.as(:attribute_value) ) }
|
99
|
+
# rule(:mailto_attribute) { equation( str('mailto').as(:attribute_name), value.as(:attribute_value)) }
|
100
|
+
# rule(:pattern_attribute) { equation( str('pattern').as(:attribute_name), value.as(:attribute_value)) }
|
101
|
+
|
102
|
+
rule(:attributes) { attribute.repeat }
|
103
|
+
rule(:valid_attr_name) { ATTRIBUTES.map{|a| str(a.to_s)}.reduce(&:|) }
|
104
|
+
|
105
|
+
rule(:value) { quoted_string | integer | string_value }
|
106
|
+
rule(:string_value) { (newline.absent? >> comment.absent? >> any).repeat.as(:unquoted_string) }
|
107
|
+
rule(:integer) { match['0-9'].repeat(1).as(:integer) }
|
108
|
+
|
109
|
+
rule(:escaped_quote) { str('\\"') }
|
110
|
+
rule(:quoted_string) { quote >> ( escaped_quote | quote.absent? >> any ).repeat.as(:quoted_string) >> quote }
|
111
|
+
|
112
|
+
rule(:comment) { str('#') >> ( newline.absent? >> any ).repeat }
|
113
|
+
rule(:newline) { str("\n") >> str("\r").maybe }
|
114
|
+
rule(:quote) { str('"') | str('\'') }
|
115
|
+
rule(:space) { match(' ').repeat(1) }
|
116
|
+
rule(:space?) { space.maybe }
|
117
|
+
|
118
|
+
rule(:eol) { space? >> comment.maybe >> newline }
|
119
|
+
|
120
|
+
def parse_snippets( snippets )
|
121
|
+
fail(ArgumentError, "Need ConfigFileSnippets") unless snippets.instance_of?(Array) and snippets.all? {|s| s.instance_of?(ConfigFileSnippet)}
|
122
|
+
parsed_tree = {}
|
123
|
+
snippets.each do |snippet|
|
124
|
+
begin
|
125
|
+
parsed_tree.merge! parse(snippet.to_s) do |key, oldval, newval|
|
126
|
+
Array(oldval) + Array(newval)
|
127
|
+
end
|
128
|
+
rescue Parslet::ParseFailed
|
129
|
+
fail ParseError.new( filename: snippet.filename, exception: $! )
|
130
|
+
end
|
131
|
+
end
|
132
|
+
Transform.new.apply( parsed_tree )
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_and_transform( text )
|
136
|
+
Transform.new.apply parse(text)
|
137
|
+
rescue Parslet::ParseFailed
|
138
|
+
raise
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Log2mail
|
2
|
+
module Config
|
3
|
+
class Section
|
4
|
+
attr_reader :name, :value
|
5
|
+
attr_accessor :attrs
|
6
|
+
def initialize( name, value = nil, attrs = [] )
|
7
|
+
@name = name.to_sym
|
8
|
+
@value = value
|
9
|
+
@attrs = attrs
|
10
|
+
end
|
11
|
+
def ==(other)
|
12
|
+
return false unless other.instance_of?(self.class)
|
13
|
+
self.name == other.name and self.value == other.value and self.attrs == other.attrs
|
14
|
+
end
|
15
|
+
def inspect
|
16
|
+
'Section %s = %s---%s' % [@name.inspect, @value.inspect, @attrs.inspect]
|
17
|
+
end
|
18
|
+
def tree
|
19
|
+
h = {}
|
20
|
+
@attrs.inject(h) do |h, a|
|
21
|
+
if a.instance_of?(Section)
|
22
|
+
name = a.name.pluralize.to_sym
|
23
|
+
unless a.attrs.empty?
|
24
|
+
apdx = {a.value => a.tree}
|
25
|
+
else
|
26
|
+
apdx = a.value
|
27
|
+
end
|
28
|
+
case apdx
|
29
|
+
when Hash
|
30
|
+
apdx.each_pair{ |k,v| ( h[name] ||= {} )[k] = v }
|
31
|
+
when Array
|
32
|
+
fail "do not know what to do"
|
33
|
+
else
|
34
|
+
( h[name] ||= {} )[apdx] = {}
|
35
|
+
end
|
36
|
+
else
|
37
|
+
name = a.name
|
38
|
+
h[name] = a.value
|
39
|
+
end
|
40
|
+
h
|
41
|
+
end
|
42
|
+
if @name==:file
|
43
|
+
return {self.value => h}
|
44
|
+
# unless @attrs.empty?
|
45
|
+
# return {self.value => h}
|
46
|
+
# else
|
47
|
+
# return self.value
|
48
|
+
# end
|
49
|
+
end
|
50
|
+
h
|
51
|
+
end
|
52
|
+
|
53
|
+
# merges sections by prefering other's attributes
|
54
|
+
# FIXME: needs specing
|
55
|
+
def +(other)
|
56
|
+
fail "Unmergable sections:\n1) #{self.inspect}\n2) #{other.inspect}\nReason: values must differ." unless self.value == other.value
|
57
|
+
@attrs.each do |a|
|
58
|
+
case a
|
59
|
+
when Attribute
|
60
|
+
other.attrs << a unless other.attrs.map(&:name).include?(a.name)
|
61
|
+
when Section
|
62
|
+
other.attrs << a
|
63
|
+
end
|
64
|
+
end
|
65
|
+
other
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|