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.
@@ -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