iniparse 0.2.1
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.
- data/LICENSE +19 -0
- data/README.rdoc +75 -0
- data/Rakefile +102 -0
- data/lib/iniparse.rb +68 -0
- data/lib/iniparse/document.rb +62 -0
- data/lib/iniparse/generator.rb +200 -0
- data/lib/iniparse/line_collection.rb +164 -0
- data/lib/iniparse/lines.rb +290 -0
- data/lib/iniparse/parser.rb +92 -0
- data/lib/iniparse/version.rb +3 -0
- data/spec/document_spec.rb +72 -0
- data/spec/fixture_spec.rb +166 -0
- data/spec/fixtures/openttd.ini +397 -0
- data/spec/fixtures/race07.ini +133 -0
- data/spec/fixtures/smb.ini +102 -0
- data/spec/generator/method_missing_spec.rb +104 -0
- data/spec/generator/with_section_blocks_spec.rb +322 -0
- data/spec/generator/without_section_blocks_spec.rb +136 -0
- data/spec/iniparse_spec.rb +21 -0
- data/spec/line_collection_spec.rb +212 -0
- data/spec/lines_spec.rb +409 -0
- data/spec/parser/document_parsing_spec.rb +50 -0
- data/spec/parser/line_parsing_spec.rb +367 -0
- data/spec/spec_fixtures.rb +46 -0
- data/spec/spec_helper.rb +164 -0
- data/spec/spec_helper_spec.rb +201 -0
- metadata +92 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
module IniParse
|
2
|
+
# Represents a collection of lines in an INI document.
|
3
|
+
#
|
4
|
+
# LineCollection acts a bit like an Array/Hash hybrid, allowing arbitrary
|
5
|
+
# lines to be added to the collection, but also indexes the keys of Section
|
6
|
+
# and Option lines to enable O(1) lookup via LineCollection#[].
|
7
|
+
#
|
8
|
+
# The lines instances are stored in an array, +@lines+, while the index of
|
9
|
+
# each Section/Option is held in a Hash, +@indicies+, keyed with the
|
10
|
+
# Section/Option#key value (see LineCollection#[]=).
|
11
|
+
#
|
12
|
+
module LineCollection
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@lines = []
|
17
|
+
@indicies = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Retrive a value identified by +key+.
|
21
|
+
def [](key)
|
22
|
+
has_key?(key) ? @lines[ @indicies[key] ] : nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set a +value+ identified by +key+.
|
26
|
+
#
|
27
|
+
# If a value with the given key already exists, the value will be replaced
|
28
|
+
# with the new one, with the new value taking the position of the old.
|
29
|
+
#
|
30
|
+
def []=(key, value)
|
31
|
+
key = key.to_s
|
32
|
+
|
33
|
+
if has_key?(key)
|
34
|
+
@lines[ @indicies[key] ] = value
|
35
|
+
else
|
36
|
+
@lines << value
|
37
|
+
@indicies[key] = @lines.length - 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Appends a line to the collection.
|
42
|
+
#
|
43
|
+
# Note that if you pass a line with a key already represented in the
|
44
|
+
# collection, the old item will be replaced.
|
45
|
+
#
|
46
|
+
def <<(line)
|
47
|
+
line.blank? ? (@lines << line) : (self[line.key] = line) ; self
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :push, :<<
|
51
|
+
|
52
|
+
# Enumerates through the collection.
|
53
|
+
#
|
54
|
+
# By default #each does not yield blank and comment lines.
|
55
|
+
#
|
56
|
+
# ==== Parameters
|
57
|
+
# include_blank<Boolean>:: Include blank/comment lines?
|
58
|
+
#
|
59
|
+
def each(include_blank = false)
|
60
|
+
@lines.each do |line|
|
61
|
+
yield(line) if include_blank || (! line.blank?)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Removes the value identified by +key+.
|
66
|
+
def delete(key)
|
67
|
+
unless (idx = @indicies[key]).nil?
|
68
|
+
@indicies.delete(key)
|
69
|
+
@indicies.each { |k,v| @indicies[k] = v -= 1 if v > idx }
|
70
|
+
@lines.delete_at(idx)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns whether +key+ is in the collection.
|
75
|
+
def has_key?(*args)
|
76
|
+
@indicies.has_key?(*args)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return an array containing the keys for the lines added to this
|
80
|
+
# collection.
|
81
|
+
def keys
|
82
|
+
map { |line| line.key }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns this collection as an array. Includes blank and comment lines.
|
86
|
+
def to_a
|
87
|
+
@lines.dup
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns this collection as a hash. Does not contain blank and comment
|
91
|
+
# lines.
|
92
|
+
def to_hash
|
93
|
+
Hash[ map { |line| [line.key, line] } ]
|
94
|
+
end
|
95
|
+
|
96
|
+
alias_method :to_h, :to_hash
|
97
|
+
end
|
98
|
+
|
99
|
+
# A implementation of LineCollection used for storing (mostly) Option
|
100
|
+
# instances contained within a Section.
|
101
|
+
#
|
102
|
+
# Since it is assumed that an INI document will only represent a section
|
103
|
+
# once, if SectionCollection encounters a Section key already held in the
|
104
|
+
# collection, the existing section is merged with the new one (see
|
105
|
+
# IniParse::Lines::Section#merge!).
|
106
|
+
class SectionCollection
|
107
|
+
include LineCollection
|
108
|
+
|
109
|
+
def <<(line)
|
110
|
+
if line.kind_of?(IniParse::Lines::Option)
|
111
|
+
raise IniParse::LineNotAllowed,
|
112
|
+
"You can't add an Option to a SectionCollection."
|
113
|
+
end
|
114
|
+
|
115
|
+
if line.blank? || (! has_key?(line.key))
|
116
|
+
super # Adding a new section, comment or blank line.
|
117
|
+
else
|
118
|
+
self[line.key].merge!(line)
|
119
|
+
end
|
120
|
+
|
121
|
+
self
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# A implementation of LineCollection used for storing (mostly) Option
|
126
|
+
# instances contained within a Section.
|
127
|
+
#
|
128
|
+
# Whenever OptionCollection encounters an Option key already held in the
|
129
|
+
# collection, it treats it as a duplicate. This means that instead of
|
130
|
+
# overwriting the existing value, the value is changed to an array
|
131
|
+
# containing the previous _and_ the new Option instances.
|
132
|
+
class OptionCollection
|
133
|
+
include LineCollection
|
134
|
+
|
135
|
+
# Appends a line to the collection.
|
136
|
+
#
|
137
|
+
# If you push an Option with a key already represented in the collection,
|
138
|
+
# the previous Option will not be overwritten, but treated as a duplicate.
|
139
|
+
#
|
140
|
+
# ==== Parameters
|
141
|
+
# line<IniParse::LineType::Line>:: The line to be added to this section.
|
142
|
+
#
|
143
|
+
def <<(line)
|
144
|
+
if line.kind_of?(IniParse::Lines::Section)
|
145
|
+
raise IniParse::LineNotAllowed,
|
146
|
+
"You can't add a Section to an OptionCollection."
|
147
|
+
end
|
148
|
+
|
149
|
+
if line.blank? || (! has_key?(line.key))
|
150
|
+
super # Adding a new option, comment or blank line.
|
151
|
+
else
|
152
|
+
self[line.key] = [self[line.key], line].flatten
|
153
|
+
end
|
154
|
+
|
155
|
+
self
|
156
|
+
end
|
157
|
+
|
158
|
+
# Return an array containing the keys for the lines added to this
|
159
|
+
# collection.
|
160
|
+
def keys
|
161
|
+
map { |line| line.kind_of?(Array) ? line.first.key : line.key }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
module IniParse
|
2
|
+
module Lines
|
3
|
+
# A base class from which other line types should inherit.
|
4
|
+
module Line
|
5
|
+
# ==== Parameters
|
6
|
+
# opts<Hash>:: Extra options for the line.
|
7
|
+
#
|
8
|
+
def initialize(opts = {})
|
9
|
+
@comment = opts.fetch(:comment, nil)
|
10
|
+
@comment_sep = opts.fetch(:comment_sep, ';')
|
11
|
+
@comment_offset = opts.fetch(:comment_offset, 0)
|
12
|
+
@indent = opts.fetch(:indent, '')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns if this line has an inline comment.
|
16
|
+
def has_comment?
|
17
|
+
not @comment.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns this line as a string as it would be represented in an INI
|
21
|
+
# document.
|
22
|
+
def to_ini
|
23
|
+
ini = line_contents
|
24
|
+
ini = @indent + ini if @indent
|
25
|
+
|
26
|
+
if has_comment?
|
27
|
+
ini += ' ' unless ini.blank?
|
28
|
+
ini = ini.ljust(@comment_offset)
|
29
|
+
ini += comment
|
30
|
+
end
|
31
|
+
|
32
|
+
ini
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the contents for this line.
|
36
|
+
def line_contents
|
37
|
+
''
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the inline comment for this line. Includes the comment
|
41
|
+
# separator at the beginning of the string.
|
42
|
+
def comment
|
43
|
+
'%s %s' % [@comment_sep, @comment]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Represents a section header in an INI document. Section headers consist
|
48
|
+
# of a string of characters wrapped in square brackets.
|
49
|
+
#
|
50
|
+
# [section]
|
51
|
+
# key=value
|
52
|
+
# etc
|
53
|
+
# ...
|
54
|
+
#
|
55
|
+
class Section
|
56
|
+
include Line
|
57
|
+
|
58
|
+
@regex = /^\[ # Opening bracket
|
59
|
+
([^\]]+) # Section name
|
60
|
+
\]$ # Closing bracket
|
61
|
+
/x
|
62
|
+
|
63
|
+
attr_accessor :key
|
64
|
+
attr_reader :lines
|
65
|
+
|
66
|
+
include Enumerable
|
67
|
+
|
68
|
+
# ==== Parameters
|
69
|
+
# key<String>:: The section name.
|
70
|
+
# opts<Hash>:: Extra options for the line.
|
71
|
+
#
|
72
|
+
def initialize(key, opts = {})
|
73
|
+
super(opts)
|
74
|
+
@key = key.to_s
|
75
|
+
@lines = IniParse::OptionCollection.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.parse(line, opts)
|
79
|
+
if m = @regex.match(line)
|
80
|
+
[:section, m[1], opts]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns this line as a string as it would be represented in an INI
|
85
|
+
# document. Includes options, comments and blanks.
|
86
|
+
def to_ini
|
87
|
+
coll = lines.to_a
|
88
|
+
|
89
|
+
if coll.any?
|
90
|
+
super + $/ + coll.to_a.map do |line|
|
91
|
+
if line.kind_of?(Array)
|
92
|
+
line.map { |dup_line| dup_line.to_ini }.join($/)
|
93
|
+
else
|
94
|
+
line.to_ini
|
95
|
+
end
|
96
|
+
end.join($/)
|
97
|
+
else
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Enumerates through each Option in this section.
|
103
|
+
#
|
104
|
+
# Does not yield blank and comment lines by default; if you want _all_
|
105
|
+
# lines to be yielded, pass true.
|
106
|
+
#
|
107
|
+
# ==== Parameters
|
108
|
+
# include_blank<Boolean>:: Include blank/comment lines?
|
109
|
+
#
|
110
|
+
def each(*args, &blk)
|
111
|
+
@lines.each(*args, &blk)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Adds a new option to this section, or updates an existing one.
|
115
|
+
#
|
116
|
+
# Note that +[]=+ has no knowledge of duplicate options and will happily
|
117
|
+
# overwrite duplicate options with your new value.
|
118
|
+
#
|
119
|
+
# section['an_option']
|
120
|
+
# # => ['duplicate one', 'duplicate two', ...]
|
121
|
+
# section['an_option'] = 'new value'
|
122
|
+
# section['an_option]
|
123
|
+
# # => 'new value'
|
124
|
+
#
|
125
|
+
# If you do not wish to overwrite duplicates, but wish instead for your
|
126
|
+
# new option to be considered a duplicate, use +add_option+ instead.
|
127
|
+
#
|
128
|
+
def []=(key, value)
|
129
|
+
@lines[key.to_s] = IniParse::Lines::Option.new(key.to_s, value)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the value of an option identified by +key+.
|
133
|
+
#
|
134
|
+
# Returns nil if there is no corresponding option. If the key provided
|
135
|
+
# matches a set of duplicate options, an array will be returned containing
|
136
|
+
# the value of each option.
|
137
|
+
#
|
138
|
+
def [](key)
|
139
|
+
key = key.to_s
|
140
|
+
|
141
|
+
if @lines.has_key?(key)
|
142
|
+
if (match = @lines[key]).kind_of?(Array)
|
143
|
+
match.map { |line| line.value }
|
144
|
+
else
|
145
|
+
match.value
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Like [], except instead of returning just the option value, it returns
|
151
|
+
# the matching line instance.
|
152
|
+
#
|
153
|
+
# Will return an array of lines if the key matches a set of duplicates.
|
154
|
+
#
|
155
|
+
def option(key)
|
156
|
+
@lines[key.to_s]
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns true if an option with the given +key+ exists in this section.
|
160
|
+
def has_option?(key)
|
161
|
+
@lines.has_key?(key.to_s)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Merges section +other+ into this one. If the section being merged into
|
165
|
+
# this one contains options with the same key, they will be handled as
|
166
|
+
# duplicates.
|
167
|
+
#
|
168
|
+
# ==== Parameters
|
169
|
+
# other<IniParse::Section>:: The section to merge into this one.
|
170
|
+
#
|
171
|
+
def merge!(other)
|
172
|
+
other.lines.each(true) do |line|
|
173
|
+
if line.kind_of?(Array)
|
174
|
+
line.each { |duplicate| @lines << duplicate }
|
175
|
+
else
|
176
|
+
@lines << line
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
#######
|
182
|
+
private
|
183
|
+
#######
|
184
|
+
|
185
|
+
def line_contents
|
186
|
+
'[%s]' % key
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Represents probably the most common type of line in an INI document:
|
191
|
+
# an option. Consists of a key and value, usually separated with an =.
|
192
|
+
#
|
193
|
+
# key = value
|
194
|
+
#
|
195
|
+
class Option
|
196
|
+
include Line
|
197
|
+
|
198
|
+
@regex = /^(.*) # Key
|
199
|
+
=
|
200
|
+
(.*?)$ # Value
|
201
|
+
/x
|
202
|
+
|
203
|
+
attr_accessor :key, :value
|
204
|
+
|
205
|
+
# ==== Parameters
|
206
|
+
# key<String>:: The option key.
|
207
|
+
# value<String>:: The value for this option.
|
208
|
+
# opts<Hash>:: Extra options for the line.
|
209
|
+
#
|
210
|
+
def initialize(key, value, opts = {})
|
211
|
+
super(opts)
|
212
|
+
@key, @value = key.to_s, value
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.parse(line, opts)
|
216
|
+
if m = @regex.match(line)
|
217
|
+
[:option, m[1].strip, typecast(m[2].strip), opts]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Attempts to typecast values.
|
222
|
+
def self.typecast(value)
|
223
|
+
case value
|
224
|
+
when /^\s*$/ then nil
|
225
|
+
when /^-?(?:\d|[1-9]\d+)$/ then Integer(value)
|
226
|
+
when /^-?(?:\d|[1-9]\d+)(?:\.\d+)?(?:e[+-]?\d+)?$/i then Float(value)
|
227
|
+
when /true/i then true
|
228
|
+
when /false/i then false
|
229
|
+
else value
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#######
|
234
|
+
private
|
235
|
+
#######
|
236
|
+
|
237
|
+
def line_contents
|
238
|
+
'%s = %s' % [key, value]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Represents a blank line. Used so that we can preserve blank lines when
|
243
|
+
# writing back to the file.
|
244
|
+
class Blank
|
245
|
+
include Line
|
246
|
+
|
247
|
+
def blank?
|
248
|
+
true
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.parse(line, opts)
|
252
|
+
if line.blank?
|
253
|
+
if opts[:comment].nil?
|
254
|
+
[:blank]
|
255
|
+
else
|
256
|
+
[:comment, opts[:comment], opts]
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Represents a comment. Comment lines begin with a semi-colon or hash.
|
263
|
+
#
|
264
|
+
# ; this is a comment
|
265
|
+
# # also a comment
|
266
|
+
#
|
267
|
+
class Comment < Blank
|
268
|
+
# Returns if this line has an inline comment.
|
269
|
+
#
|
270
|
+
# Being a Comment this will always return true, even if the comment
|
271
|
+
# is nil. This would be the case if the line starts with a comment
|
272
|
+
# seperator, but has no comment text. See spec/fixtures/smb.ini for a
|
273
|
+
# real-world example.
|
274
|
+
#
|
275
|
+
def has_comment?
|
276
|
+
true
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns the inline comment for this line. Includes the comment
|
280
|
+
# separator at the beginning of the string.
|
281
|
+
#
|
282
|
+
# In rare cases where a comment seperator appeared in the original file,
|
283
|
+
# but without a comment, just the seperator will be returned.
|
284
|
+
#
|
285
|
+
def comment
|
286
|
+
@comment.blank? ? @comment_sep : super
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end # Lines
|
290
|
+
end # IniParse
|