ini_file 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Jerry D'Antonio
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1 @@
1
+ README for ini_file
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "rubygems"
2
+ require "bundler/gem_tasks"
3
+ require "rspec"
4
+ require "rspec/core/rake_task"
5
+
6
+ $:.unshift 'tasks'
7
+ Dir.glob('tasks/**/*.rake').each do|rakefile|
8
+ load rakefile
9
+ end
10
+
11
+ Bundler::GemHelper.install_tasks
12
+ RSpec::Core::RakeTask.new(:spec) do |t|
13
+ t.rspec_opts = '-fd --color'
14
+ end
15
+
16
+ #task :default => [:default_task]
data/lib/ini_file.rb ADDED
@@ -0,0 +1,32 @@
1
+ $:.push File.join(File.dirname(__FILE__))
2
+
3
+ require "ini_file/content"
4
+ require "ini_file/parser"
5
+ require "ini_file/version"
6
+
7
+ # http://en.wikipedia.org/wiki/INI_file
8
+
9
+ module IniFile
10
+ extend self
11
+
12
+ def load(path, safe = false)
13
+ contents = File.open(File.expand_path(path), 'r') {|f| f.read }
14
+ return Content.parse(contents)
15
+ rescue
16
+ if safe
17
+ return nil
18
+ else
19
+ raise $!
20
+ end
21
+ end
22
+
23
+ def parse(contents, safe = false)
24
+ return Parser.parse(contents)
25
+ rescue
26
+ if safe
27
+ return nil
28
+ else
29
+ raise $!
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ require 'ini_file/parser'
2
+
3
+ module IniFile
4
+
5
+ class Content
6
+
7
+ attr_reader :name
8
+
9
+ def empty?
10
+ return @content.empty?
11
+ end
12
+
13
+ def [](key)
14
+ if @content[key].is_a? Hash
15
+ return Content.from_hash(key, @content[key])
16
+ else
17
+ return @content[key]
18
+ end
19
+ end
20
+
21
+ def to_hash
22
+ return Marshal.load( Marshal.dump(@content) )
23
+ end
24
+
25
+ def each(&block)
26
+ @content.each do |key, value|
27
+ yield(key, value) unless value.is_a? Hash
28
+ end
29
+ end
30
+
31
+ def each_section(&block)
32
+ @content.each do |key, value|
33
+ yield(Content.from_hash(key, value)) if value.is_a? Hash
34
+ end
35
+ end
36
+
37
+ def method_missing(method, *args, &block)
38
+
39
+ key = method.to_s.downcase.to_sym
40
+
41
+ if @content.has_key?(key)
42
+ if args.count > 0
43
+ raise ArgumentError.new("wrong number of arguments(#{args.count} for 0)")
44
+ elsif block_given?
45
+ raise ArgumentError.new("block syntax not supported")
46
+ elsif @content[key].is_a? Hash
47
+ return Content.from_hash(key, @content[key])
48
+ else
49
+ return @content[key]
50
+ end
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def self.parse(contents)
57
+ content = Content.new
58
+ content.instance_variable_set(:@content, Parser.parse(contents))
59
+ return content
60
+ end
61
+
62
+ private
63
+
64
+ def initialize(name = nil)
65
+ @name = name
66
+ @content = {}
67
+ end
68
+
69
+ def self.from_hash(name, contents)
70
+ content = Content.new(name)
71
+ content.instance_variable_set(:@content, contents)
72
+ return content
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,93 @@
1
+ module IniFile
2
+
3
+ IniFormatError = Class.new(StandardError)
4
+
5
+ module Parser
6
+ extend self
7
+
8
+ def parse(contents)
9
+ raise ArgumentError.new('contents cannot be nil') if contents.nil?
10
+ raise ArgumentError.new('contents must be a string') unless contents.is_a? String
11
+ raise ArgumentError.new('contents cannot be blank') if contents.strip.empty?
12
+
13
+ root = {}
14
+
15
+ section_pattern = /^\s*\[([^\]]+)\]\s*$/
16
+ property_pattern = /^\s*([^;#:=\r\n]++)\s*[:=](.*)$/
17
+ comment_pattern = /^\s*([;#].*)$/
18
+
19
+ pattern = /#{section_pattern}|#{property_pattern}|#{comment_pattern}|^(.*)$/
20
+
21
+ current = root
22
+
23
+ contents.scan(pattern) do |section, key, value, comment, garbage|
24
+
25
+ if section
26
+ if section.scan(/[\.\\\/,]/).uniq.size > 1
27
+ raise IniFormatError.new("Section hierarchy names must use one delimiter: #{section}")
28
+ end
29
+ sections = section.strip.split(/\s*[\.\\\/,\s]{1}\s*/)
30
+ if sections.empty?
31
+ raise IniFormatError.new("Section names cannot be blank: #{section}")
32
+ end
33
+
34
+ current = root
35
+
36
+ sections.each do |section|
37
+ section = $1 || $2 if section =~ /^"([^"]+)"$|^'([^']+)'$/
38
+ section = section.strip.downcase.to_sym
39
+ if section.empty?
40
+ raise IniFormatError.new("Section names cannot be blank: #{section}")
41
+ elsif section =~ /[\W]+/
42
+ raise IniFormatError.new("Section names cannot contain punctuation: #{section}")
43
+ elsif current.has_key?(section) && !current[section].is_a?(Hash)
44
+ raise IniFormatError.new("Section name matches existing property name: #{section}")
45
+ else
46
+ current[section] = {} unless current.has_key?(section)
47
+ current = current[section]
48
+ end
49
+ end
50
+
51
+ elsif key && value
52
+ key = key.strip.downcase.gsub(/-/, '_').to_sym
53
+ value = value.strip
54
+ if key.empty?
55
+ raise IniFormatError.new("Property names cannot be blank: #{key}")
56
+ elsif current[key]
57
+ raise IniFormatError.new("Duplicate keys are not allowed: #{key}")
58
+ elsif key =~ /[\W\s]+/
59
+ raise IniFormatError.new("Property names cannot contain spaces or punctuation: #{key}")
60
+ end
61
+ value = value.gsub(/\s+/, ' ') unless value =~ /^"(.+)"$|^'(.+)'$/
62
+ if value =~ /^\d+$/
63
+ value = value.to_i
64
+ elsif value =~ /^\d*\.\d+$/
65
+ value = value.to_f
66
+ else
67
+ if value =~ /^"(.+)"$|^'(.+)'$/
68
+ value = $1 || $2
69
+ else
70
+ value = value.gsub(/\s*[^\\]{1}[#;].*$/, '')
71
+ value = value.gsub(/\\([#;])/, '\1')
72
+ end
73
+ value = value.gsub(/\\[0abtrn\\]/) {|s| eval('"%s"' % "#{s}") }
74
+ value = value.gsub(/\\[ux](\d{4})/) {|s| eval('"%s"' % "\\u#{$1}") }
75
+ end
76
+ current[key] = value
77
+
78
+ elsif comment
79
+ # do nothing
80
+ elsif garbage
81
+ raise IniFormatError.new("Unrecognized pattern: #{garbage}")
82
+ else
83
+ # possibly throw exceptions?
84
+ end
85
+ end
86
+
87
+ root.freeze
88
+ return root
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,3 @@
1
+ module IniFile
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,335 @@
1
+ require 'spec_helper'
2
+
3
+ module IniFile
4
+
5
+ describe Content do
6
+
7
+ let(:contents) do
8
+ <<-DATA
9
+ ; this line is a comment
10
+ KEY1 = value1
11
+ key2 = "The second value"
12
+ the-third-key = 3
13
+
14
+ [section_1]
15
+ key3: value3
16
+
17
+ [section_1.sub_1]
18
+ sub_1_key: 'This is the subsection key'
19
+
20
+ [section_1/sub_1/sub_2]
21
+ key: value
22
+
23
+ [section_1 \ sub_1 \ sub_2 \ sub_3]
24
+ key: value
25
+
26
+ [section_1,sub_1,sub_2,sub_3,sub_4]
27
+ key: value
28
+ DATA
29
+ end
30
+
31
+ context '#new' do
32
+
33
+ it 'calls Parser.parse with the contents' do
34
+ ini_contents = 'key=value'
35
+ Parser.should_receive(:parse).with(ini_contents)
36
+ Content.parse(ini_contents)
37
+ end
38
+
39
+ end
40
+
41
+ context 'accessor methods' do
42
+
43
+ context 'attribute reader' do
44
+
45
+ subject { Content.parse(contents) }
46
+
47
+ it 'throws an ArgumentError if parameters are passed' do
48
+ lambda { subject.key1(nil) }.should raise_error(ArgumentError)
49
+ end
50
+
51
+ it 'throws an ArgumentError if a block is provided' do
52
+ lambda { subject.key1{|x| nil} }.should raise_error(ArgumentError)
53
+ end
54
+
55
+ it 'converts global property keys to attributes at the root' do
56
+ subject.key1.should_not be_nil
57
+ end
58
+
59
+ it 'converts global property values to string values at the root' do
60
+ subject.key1.should eq 'value1'
61
+ end
62
+
63
+ it 'ignores case of property keys' do
64
+ subject.KEY1.should eq 'value1'
65
+ end
66
+
67
+ it 'replaces dashes in the key name with underscores' do
68
+ subject.the_third_key.should eq 3
69
+ end
70
+
71
+ it 'converts section names to attributes at the root' do
72
+ subject.section_1.should_not be_nil
73
+ end
74
+
75
+ it 'converts sections to nested attributes under the root' do
76
+ subject.section_1[:key3].should eq 'value3'
77
+ end
78
+
79
+ it 'converts section property keys to attributes under the section' do
80
+ subject.section_1.key3.should eq 'value3'
81
+ end
82
+
83
+ it 'converts section property values to string values under the section' do
84
+ section = subject.section_1
85
+ value = subject.section_1.key3
86
+ subject.section_1.key3.should eq 'value3'
87
+ end
88
+
89
+ it 'converts section hierarchies to nested section hierarchies' do
90
+ subject.section_1.sub_1.sub_2.sub_3.sub_4.should_not be_nil
91
+ end
92
+
93
+ it 'ignores case of section names' do
94
+ subject.SECTION_1[:key3].should eq 'value3'
95
+ end
96
+
97
+ it 'ignores case of section hierarchies' do
98
+ subject.section_1.SUB_1.should_not be_nil
99
+ end
100
+
101
+ it 'ignores case of property names in section hierarchies' do
102
+ subject.section_1.sub_1.SUB_1_KEY.should eq 'This is the subsection key'
103
+ end
104
+
105
+ it 'throws NoMethodError when accessing a non-existing property' do
106
+ lambda { subject.bogus }.should raise_error(NoMethodError)
107
+ end
108
+
109
+ it 'throws NoMethodError when accessing a non-existing section' do
110
+ lambda { subject.bogus[:key1] }.should raise_error(NoMethodError)
111
+ end
112
+
113
+ it 'throws NoMethodError when accessing a non-existing subsection' do
114
+ lambda { subject.section_1.bogus }.should raise_error(NoMethodError)
115
+ end
116
+
117
+ it 'throws NoMethodError when accessing a non-existing subsection property' do
118
+ lambda { subject.section_1.sub.bogus }.should raise_error(NoMethodError)
119
+ end
120
+
121
+ end
122
+
123
+ context '#[]' do
124
+
125
+ subject { Content.parse(contents) }
126
+
127
+ it 'converts global property keys to symbols at the root' do
128
+ subject[:key1].should_not be_nil
129
+ subject[:key2].should_not be_nil
130
+ end
131
+
132
+ it 'converts global property values to string values at the root' do
133
+ subject[:key1].should eq 'value1'
134
+ subject[:key2].should eq 'The second value'
135
+ end
136
+
137
+ it 'converts section names to symbols at the root' do
138
+ subject[:section_1].should_not be_nil
139
+ end
140
+
141
+ it 'converts sections to nodes' do
142
+ subject[:section_1].should be_kind_of IniFile::Content
143
+ end
144
+
145
+ it 'converts section property keys to symbols in the section hash' do
146
+ subject[:section_1][:key3].should_not be_nil
147
+ end
148
+
149
+ it 'converts section property values to string values in the section hash' do
150
+ subject[:section_1][:key3].should eq 'value3'
151
+ end
152
+
153
+ it 'converts section hierarchies to node hierarchies' do
154
+ subject[:section_1].should be_kind_of IniFile::Content
155
+ subject[:section_1][:sub_1].should be_kind_of IniFile::Content
156
+ subject[:section_1][:sub_1][:sub_2].should be_kind_of IniFile::Content
157
+ subject[:section_1][:sub_1][:sub_2][:sub_3].should be_kind_of IniFile::Content
158
+ subject[:section_1][:sub_1][:sub_2][:sub_3][:sub_4].should be_kind_of IniFile::Content
159
+ end
160
+
161
+ end
162
+
163
+ context '#to_hash' do
164
+
165
+ subject { Content.parse(contents).to_hash }
166
+
167
+ it 'converts global property keys to symbols at the root' do
168
+ subject[:key1].should_not be_nil
169
+ subject[:key2].should_not be_nil
170
+ end
171
+
172
+ it 'converts global property values to string values at the root' do
173
+ subject[:key1].should eq 'value1'
174
+ subject[:key2].should eq 'The second value'
175
+ end
176
+
177
+ it 'converts section names to symbols at the root' do
178
+ subject[:section_1].should_not be_nil
179
+ end
180
+
181
+ it 'converts sections to hash values at the root' do
182
+ subject[:section_1].should be_a Hash
183
+ end
184
+
185
+ it 'converts section property keys to symbols in the section hash' do
186
+ subject[:section_1][:key3].should_not be_nil
187
+ end
188
+
189
+ it 'converts section property values to string values in the section hash' do
190
+ subject[:section_1][:key3].should eq 'value3'
191
+ end
192
+
193
+ it 'converts section hierarchies to hash hierarchies' do
194
+ subject[:section_1][:sub_1][:sub_2][:sub_3][:sub_4].should be_a Hash
195
+ end
196
+
197
+ end
198
+
199
+ end
200
+
201
+ context 'iterators' do
202
+
203
+ let(:contents) do
204
+ <<-DATA
205
+ ; this line is a comment
206
+ KEY1 = value1
207
+ key2 = "The second value"
208
+
209
+ [section_1]
210
+ key1-1: value1
211
+ key1-2: value2
212
+ key1-3: value3
213
+ key1-4: value4
214
+
215
+ [section_2]
216
+ key2-1: value1
217
+ key2-2: value2
218
+ key2-3: value3
219
+ key2-4: value4
220
+
221
+ [section_1.sub_1]
222
+ key1-1-1: value1
223
+ key1-1-2: value2
224
+ key1-1-3: value3
225
+ key1-1-4: value4
226
+
227
+ [section_1.sub_2]
228
+ key1-2-1: value1
229
+ key1-2-2: value2
230
+ key1-2-3: value3
231
+ key1-2-4: value4
232
+ DATA
233
+ end
234
+
235
+ let(:keys) { [ :key1, :key2 ] }
236
+ let(:sections) { [ :section_1, :section_2 ] }
237
+ let(:section_keys) { [ :key1_1, :key1_2, :key1_3, :key1_4 ] }
238
+ let(:subsections) { [ :sub_1, :sub_2 ] }
239
+ let(:subsection_keys) { [ :key2_1, :key2_2, :key2_3, :key2_4 ] }
240
+ let(:subsection_1_keys) { [ :key1_1_1, :key1_1_2, :key1_1_3, :key1_1_4 ] }
241
+ let(:subsection_2_keys) { [ :key1_2_1, :key1_2_2, :key1_2_3, :key1_2_4 ] }
242
+
243
+ subject { Content.parse(contents) }
244
+
245
+ context '#each' do
246
+
247
+ it 'iterates over each key/value pair in the global section' do
248
+ subject.each do |key, value|
249
+ keys.should include key
250
+ end
251
+ end
252
+
253
+ it 'returns the value associated with the current key' do
254
+ subject.each do |key, value|
255
+ subject[key].should eq value
256
+ end
257
+ end
258
+
259
+ it 'does not iterate over sections at the root' do
260
+ subject.each do |key, value|
261
+ sections.should_not include key
262
+ end
263
+ end
264
+
265
+ end
266
+
267
+ context '#each_section' do
268
+
269
+ it 'iterates over each section in the root' do
270
+ subject.each_section do |section|
271
+ sections.should include section.name
272
+ end
273
+ end
274
+
275
+ it 'returns the node associated with the current section' do
276
+ subject.each_section do |section|
277
+ section.should be_kind_of IniFile::Content
278
+ end
279
+ end
280
+
281
+ it 'does not iterate over properties at the root' do
282
+ subject.each_section do |section|
283
+ keys.should_not include section.name
284
+ end
285
+ end
286
+
287
+ end
288
+
289
+ context '#each for sections' do
290
+
291
+ it 'iterates over each key/value pair in the given section' do
292
+ subject.section_1.each do |key, value|
293
+ section_keys.should include key
294
+ end
295
+ end
296
+
297
+ it 'returns the value associated with the current key' do
298
+ subject.section_1.each do |key, value|
299
+ subject.section_1[key].should eq value
300
+ end
301
+ end
302
+
303
+ it 'does not iterate over subsections of the given section' do
304
+ subject[:section_1].each do |key, value|
305
+ subsections.should_not include key
306
+ end
307
+ end
308
+
309
+ end
310
+
311
+ context '#each_section for sections' do
312
+
313
+ it 'iterates over each section in the root' do
314
+ subject.section_1.each_section do |section|
315
+ subsections.should include section.name
316
+ end
317
+ end
318
+
319
+ it 'returns the node associated with the current section' do
320
+ subject.section_1.each_section do |section|
321
+ section.should be_kind_of IniFile::Content
322
+ end
323
+ end
324
+
325
+ it 'does not iterate over properties at the root' do
326
+ subject.section_1.each_section do |section|
327
+ subsection_keys.should_not include section.name
328
+ end
329
+ end
330
+
331
+ end
332
+
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,499 @@
1
+ require 'spec_helper'
2
+
3
+ # http://en.wikipedia.org/wiki/INI_file
4
+
5
+ module IniFile
6
+
7
+ describe Parser do
8
+
9
+ context '#parse' do
10
+
11
+ context 'method call' do
12
+
13
+ let(:contents) { "; this is a comment" }
14
+
15
+ it 'requires the first argument to be a string' do
16
+ lambda { subject.parse(contents) }.should_not raise_error
17
+ end
18
+
19
+ it 'throws an exception if the first argument is nil' do
20
+ lambda { subject.parse(nil) }.should raise_error(ArgumentError)
21
+ end
22
+
23
+ it 'throws an exception if the first argument is an empty string' do
24
+ lambda { subject.parse('') }.should raise_error(ArgumentError)
25
+ end
26
+
27
+ it 'throws an exception if the first argument is nothing but whitespace' do
28
+ lambda { subject.parse(' ') }.should raise_error(ArgumentError)
29
+ end
30
+
31
+ it 'throws an exception if the first argument is not a string' do
32
+ lambda { subject.parse(123) }.should raise_error(ArgumentError)
33
+ end
34
+
35
+ end
36
+
37
+ context 'return value' do
38
+
39
+ it 'is a hash on success' do
40
+ subject.parse('key=value').should be_a Hash
41
+ end
42
+
43
+ it 'is an empty hash when the content is only comments' do
44
+ subject.parse('; this is a comment').should be_empty
45
+ end
46
+
47
+ end
48
+
49
+ context 'content' do
50
+
51
+ context 'properties' do
52
+
53
+ it 'extracts the key from the left of the delimiter' do
54
+ ini = subject.parse('key=value')
55
+ ini[:key].should_not be_nil
56
+ end
57
+
58
+ it 'ignores whitespace before the key' do
59
+ ini = subject.parse(' key=value')
60
+ ini[:key].should eq 'value'
61
+ end
62
+
63
+ it 'throws an exception on a blank key' do
64
+ lambda {
65
+ subject.parse(' =value')
66
+ }.should raise_error(IniFormatError)
67
+ end
68
+
69
+ it 'throws an exception on whitespace within the key' do
70
+ lambda {
71
+ subject.parse('this is the key=value')
72
+ }.should raise_error(IniFormatError)
73
+ end
74
+
75
+ it 'allows underscores in the key' do
76
+ ini = subject.parse('main_window_maximized=value')
77
+ ini[:main_window_maximized].should eq 'value'
78
+ end
79
+
80
+ it 'converts dashes in the key to underscores' do
81
+ ini = subject.parse('main-window-maximized=value')
82
+ ini[:main_window_maximized].should eq 'value'
83
+ end
84
+
85
+ it 'throws an exception on punctuation within the key' do
86
+ lambda {
87
+ subject.parse("the'key=value")
88
+ }.should raise_error(IniFormatError)
89
+ end
90
+
91
+ it 'throws an exception for a duplicate key' do
92
+ lambda {
93
+ subject.parse("key=value1\nkey=value2")
94
+ }.should raise_error(IniFormatError)
95
+ end
96
+
97
+ it 'ignores the case of the key' do
98
+ ini = subject.parse('KEY=value')
99
+ ini[:key].should eq 'value'
100
+ end
101
+
102
+ it 'allows an equal sign as the delimiter' do
103
+ ini = subject.parse('key=value')
104
+ ini[:key].should eq 'value'
105
+ end
106
+
107
+ it 'allows a colon as the delimiter' do
108
+ ini = subject.parse('key:value')
109
+ ini[:key].should eq 'value'
110
+ end
111
+
112
+ it 'allows spaces before the delimiter' do
113
+ ini = subject.parse('key =value')
114
+ ini[:key].should eq 'value'
115
+ end
116
+
117
+ it 'allows spaces after the delimiter' do
118
+ ini = subject.parse('key: value')
119
+ ini[:key].should eq 'value'
120
+ end
121
+
122
+ it 'ignores single quotes around the value' do
123
+ ini = subject.parse("key='value'")
124
+ ini[:key].should eq 'value'
125
+ end
126
+
127
+ it 'ignores double quotes around the value' do
128
+ ini = subject.parse('key="value"')
129
+ ini[:key].should eq 'value'
130
+ end
131
+
132
+ it 'allows a blank value' do
133
+ ini = subject.parse('key=')
134
+ ini[:key].should be_empty
135
+ end
136
+
137
+ it 'allows spaces within the value' do
138
+ ini = subject.parse('key=this is the value')
139
+ ini[:key].should eq 'this is the value'
140
+ end
141
+
142
+ it 'allows punctuation within the value' do
143
+ value = "+refs/heads/*:refs/remotes/origin/*"
144
+ ini = subject.parse("fetch = #{value}")
145
+ ini[:fetch].should eq value
146
+
147
+ value = "url = git@github.com:jdantonio/ini_file.git"
148
+ ini = subject.parse("url = #{value}")
149
+ ini[:url].should eq value
150
+ end
151
+
152
+ it 'collapses concurrent space characters within the value' do
153
+ ini = subject.parse("key=this is\tthe value")
154
+ ini[:key].should eq 'this is the value'
155
+ end
156
+
157
+ it 'does not collapse concurrent space within quoted values' do
158
+ ini = subject.parse("key='this is\tthe value'")
159
+ ini[:key].should eq "this is\tthe value"
160
+ end
161
+
162
+ it 'preserves the case of the value' do
163
+ ini = subject.parse('key=This is the VALUE')
164
+ ini[:key].should eq 'This is the VALUE'
165
+ end
166
+
167
+ it 'allows line continuation when a line ends with a backslash' do
168
+ pending
169
+ ini = subject.parse("key=this\\\nvalue")
170
+ ini[:key].should eq 'this value'
171
+ end
172
+
173
+ end
174
+
175
+ context 'escape sequences' do
176
+
177
+ it "converts \\\\ into \\" do
178
+ ini = subject.parse("key=this\\\\value")
179
+ ini[:key].should eq "this\\value"
180
+ end
181
+
182
+ it "converts \\0 into a null character" do
183
+ ini = subject.parse("key=this\\0value")
184
+ ini[:key].should eq "this\0value"
185
+ end
186
+
187
+ it "converts \\a into a bell/alert" do
188
+ ini = subject.parse("key=this\\avalue")
189
+ ini[:key].should eq "this\avalue"
190
+ end
191
+
192
+ it "converts \\b into a bell/alert" do
193
+ ini = subject.parse("key=this\\bvalue")
194
+ ini[:key].should eq "this\bvalue"
195
+ end
196
+
197
+ it "converts \\t into a tab" do
198
+ ini = subject.parse("key=this\\tvalue")
199
+ ini[:key].should eq "this\tvalue"
200
+ end
201
+
202
+ it "converts \\r into carriage return" do
203
+ ini = subject.parse("key=this\\rvalue")
204
+ ini[:key].should eq "this\rvalue"
205
+ end
206
+
207
+ it "converts \\n into a newline" do
208
+ ini = subject.parse("key=this\\nvalue")
209
+ ini[:key].should eq "this\nvalue"
210
+ end
211
+
212
+ it "converts \\; into a semicolon" do
213
+ ini = subject.parse("key=this\\;value")
214
+ ini[:key].should eq "this;value"
215
+ end
216
+
217
+ it "converts \\# into a number sign" do
218
+ ini = subject.parse("key=this\\#value")
219
+ ini[:key].should eq "this#value"
220
+ end
221
+
222
+ it 'converts \\x???? into a hexidecimal character' do
223
+ ini = subject.parse("key=this\\x1234value")
224
+ ini[:key].should eq "this\u1234value"
225
+ end
226
+
227
+ it 'converts \\u???? into a hexidecimal character' do
228
+ ini = subject.parse("key=this\\u1234value")
229
+ ini[:key].should eq "this\u1234value"
230
+ end
231
+
232
+ end
233
+
234
+ context 'comments' do
235
+
236
+ it 'ignores lines beginning with a semicolon' do
237
+ ini = subject.parse(';this is a comment')
238
+ ini.should be_empty
239
+ end
240
+
241
+ it 'ignores lines beginning with a pound sign' do
242
+ ini = subject.parse('#this is a comment')
243
+ ini.should be_empty
244
+ end
245
+
246
+ it 'ignores inline comments beginning with a semicolon in unquoted values' do
247
+ ini = subject.parse("key = value ; comment")
248
+ ini[:key].should eq "value"
249
+ end
250
+
251
+ it 'ignores inline comments beginning with a pound sign in unquoted values' do
252
+ ini = subject.parse("key = value # comment")
253
+ ini[:key].should eq "value"
254
+ end
255
+
256
+ it 'allows inline comments beginning with a semicolon in quoted values' do
257
+ ini = subject.parse("key = \"value ; comment\"")
258
+ ini[:key].should eq "value ; comment"
259
+ end
260
+
261
+ it 'allows inline comments beginning with a pound sign in quoted values' do
262
+ ini = subject.parse('key = "value # comment"')
263
+ ini[:key].should eq 'value # comment'
264
+ end
265
+
266
+ it 'allows equal signs within comments' do
267
+ ini = subject.parse(' ;editor = mate -w')
268
+ ini.should be_empty
269
+ end
270
+
271
+ it 'allows colons within comments' do
272
+ ini = subject.parse(' #editor = mate -w')
273
+ ini.should be_empty
274
+ end
275
+
276
+ it 'ignores spaces before the comment indicator' do
277
+ ini = subject.parse(' # this is a comment')
278
+ ini.should be_empty
279
+ end
280
+
281
+ it 'ignores blank lines' do
282
+ ini = subject.parse("key1=value1\n \n\t\n \t key2=value2\n#comment")
283
+ ini[:key1].should eq 'value1'
284
+ ini[:key2].should eq 'value2'
285
+ end
286
+
287
+ it 'ignores empty lines' do
288
+ ini = subject.parse("key1=value1\n\n\n\nkey2=value2")
289
+ ini[:key1].should eq 'value1'
290
+ ini[:key2].should eq 'value2'
291
+ end
292
+
293
+ end
294
+
295
+ context 'sections' do
296
+
297
+ it 'assigns properties before any section headers to the global section' do
298
+ ini = subject.parse("key1=value1\n[section]\nkey2=value2")
299
+ ini[:key1].should eq 'value1'
300
+ end
301
+
302
+ it 'assigns properties after a section header to that section' do
303
+ ini = subject.parse("[section]\nkey=value")
304
+ ini[:key].should be_nil
305
+ ini[:section][:key].should eq 'value'
306
+ end
307
+
308
+ it 'allows whitespace before the opening bracket' do
309
+ ini = subject.parse(" [section]\nkey=value")
310
+ ini[:key].should be_nil
311
+ ini[:section][:key].should eq 'value'
312
+ end
313
+
314
+ it 'ignores whitespace between the opening bracket and the section name' do
315
+ ini = subject.parse("[ section]\nkey=value")
316
+ ini[:key].should be_nil
317
+ ini[:section][:key].should eq 'value'
318
+ end
319
+
320
+ it 'ignores whitespace between the closing bracket and the section name' do
321
+ ini = subject.parse("[section ]\nkey=value")
322
+ ini[:key].should be_nil
323
+ ini[:section][:key].should eq 'value'
324
+ end
325
+
326
+ it 'allows whitespace after the closing bracket' do
327
+ ini = subject.parse("[section] \nkey=value")
328
+ ini[:key].should be_nil
329
+ ini[:section][:key].should eq 'value'
330
+ end
331
+
332
+ it 'allows section names to be enclosed in double quotes' do
333
+ ini = subject.parse("[ \"section\" ]\nkey=value")
334
+ ini[:key].should be_nil
335
+ ini[:section][:key].should eq 'value'
336
+ end
337
+
338
+ it 'allows section names to be enclosed in single quotes' do
339
+ ini = subject.parse("[ 'section' ]\nkey=value")
340
+ ini[:key].should be_nil
341
+ ini[:section][:key].should eq 'value'
342
+ end
343
+
344
+ it 'throws an exception on punctuation within the section name' do
345
+ lambda {
346
+ subject.parse("[the'section]\nkey=value")
347
+ }.should raise_error(IniFormatError)
348
+ end
349
+
350
+ it 'throws an exception when a section name is the same as a property key' do
351
+ lambda {
352
+ subject.parse("foo=value1\n[foo]\nkey2=value2")
353
+ }.should raise_error(IniFormatError)
354
+ end
355
+
356
+ it 'throws an exception on duplicate keys within a section' do
357
+ lambda {
358
+ subject.parse("[section]\nfoo=value1\n[section]\nfoo=value2")
359
+ }.should raise_error(IniFormatError)
360
+ end
361
+
362
+ it 'ignores the case of the section name' do
363
+ ini = subject.parse("[SeCtIoN]\nkey=value")
364
+ ini[:key].should be_nil
365
+ ini[:section][:key].should eq 'value'
366
+ end
367
+
368
+ it 'creates a hierarchy when a section name is delimited by a dot' do
369
+ ini = subject.parse("[section.subsection]\nkey=value")
370
+ ini[:key].should be_nil
371
+ ini[:section][:subsection][:key].should eq 'value'
372
+ end
373
+
374
+ it 'creates a hierarchy when a section name is delimited by a backslash' do
375
+ ini = subject.parse("[section\\subsection]\nkey=value")
376
+ ini[:key].should be_nil
377
+ ini[:section][:subsection][:key].should eq 'value'
378
+ end
379
+
380
+ it 'creates a hierarchy when a section name is delimited by a forward slash' do
381
+ ini = subject.parse("[section/subsection]\nkey=value")
382
+ ini[:key].should be_nil
383
+ ini[:section][:subsection][:key].should eq 'value'
384
+ end
385
+
386
+ it 'creates a hierarchy when a section name is delimited by a comma' do
387
+ ini = subject.parse("[section,subsection]\nkey=value")
388
+ ini[:key].should be_nil
389
+ ini[:section][:subsection][:key].should eq 'value'
390
+ end
391
+
392
+ it 'creates a hierarchy when a section name is delimited by whitespace' do
393
+ ini = subject.parse("[section subsection]\nkey=value")
394
+ ini[:key].should be_nil
395
+ ini[:section][:subsection][:key].should eq 'value'
396
+ end
397
+
398
+ it 'allows whitespace before the section delimiter' do
399
+ ini = subject.parse("[section .subsection]\nkey=value")
400
+ ini[:key].should be_nil
401
+ ini[:section][:subsection][:key].should eq 'value'
402
+ end
403
+
404
+ it 'allows whitespace after the section delimiter' do
405
+ ini = subject.parse("[section.\t subsection]\nkey=value")
406
+ ini[:key].should be_nil
407
+ ini[:section][:subsection][:key].should eq 'value'
408
+ end
409
+
410
+ it 'does not collide names when a section has subsections' do
411
+ ini = subject.parse("[section]\nkey1=value1\n[section.subsection]\nkey2=value2")
412
+ ini[:section].should be_a Hash
413
+ ini[:section][:key1].should eq 'value1'
414
+ ini[:section].count.should eq 2
415
+ ini[:section][:subsection].should be_a Hash
416
+ ini[:section][:subsection][:key2].should eq 'value2'
417
+ ini[:section][:subsection].count.should eq 1
418
+ end
419
+
420
+ it 'allows a section to have a subsection with the same name' do
421
+ ini = subject.parse("[section]\nkey1=value1\n[section.section]\nkey2=value2")
422
+ ini[:section].should be_a Hash
423
+ ini[:section][:key1].should eq 'value1'
424
+ ini[:section].count.should eq 2
425
+ ini[:section][:section].should be_a Hash
426
+ ini[:section][:section][:key2].should eq 'value2'
427
+ ini[:section][:section].count.should eq 1
428
+ end
429
+
430
+ it 'throws an exception when a section name is blank' do
431
+ lambda {
432
+ subject.parse("[ ]")
433
+ }.should raise_error(IniFormatError)
434
+ end
435
+
436
+ it 'throws an exception when a subsection name is blank' do
437
+ lambda {
438
+ subject.parse("[head..subsection]")
439
+ }.should raise_error(IniFormatError)
440
+ end
441
+
442
+ it 'throws an exception when a section name mixes hierarchy delimiters' do
443
+ lambda {
444
+ subject.parse("[this.is\\the/section,header]")
445
+ }.should raise_error(IniFormatError)
446
+ end
447
+
448
+ end
449
+
450
+ context 'multiple lines' do
451
+
452
+ it 'supports multiple property lines' do
453
+ ini = subject.parse("key1=value1\nkey2=value2")
454
+ ini[:key1].should eq 'value1'
455
+ ini[:key2].should eq 'value2'
456
+ end
457
+
458
+ it 'supports multiple comment lines' do
459
+ ini = subject.parse(";this is a comment\n#this is also a comment")
460
+ ini.should be_empty
461
+ end
462
+
463
+ it 'supports multiple blank lines' do
464
+ ini = subject.parse("key1=value1\n\n\n\n\n\nkey2=value2")
465
+ ini[:key1].should eq 'value1'
466
+ ini[:key2].should eq 'value2'
467
+ end
468
+
469
+ end
470
+
471
+ context 'garbage' do
472
+
473
+ it 'throws an exception when encountering garbage lines' do
474
+ lambda {
475
+ subject.parse('this line is garbage')
476
+ }.should raise_error(IniFormatError)
477
+ end
478
+
479
+ end
480
+
481
+ context 'property data types' do
482
+
483
+ it 'converts non-quoted integer property values to integers' do
484
+ ini = subject.parse("key1 = 123")
485
+ ini[:key1].should be_a Numeric
486
+ ini[:key1].should eq 123
487
+ end
488
+
489
+ it 'converts non-quoted numeric property values to numbers' do
490
+ ini = subject.parse("key1 = 123.45")
491
+ ini[:key1].should be_a Numeric
492
+ ini[:key1].should be_within(0.1).of(123.45)
493
+ end
494
+ end
495
+
496
+ end
497
+ end
498
+ end
499
+ end
@@ -0,0 +1,75 @@
1
+ require 'fileutils'
2
+ require 'spec_helper'
3
+
4
+ # http://en.wikipedia.org/wiki/INI_file
5
+
6
+ describe IniFile do
7
+
8
+ subject { IniFile }
9
+
10
+ let(:filename) { 'test.ini' }
11
+ let(:contents) { '; this is a comment' }
12
+
13
+ def write_ini_file(path = '.')
14
+ File.open(File.join(path, filename), 'w') {|f| f.write(contents) }
15
+ end
16
+
17
+ context '#parse' do
18
+
19
+ it 'returns a hash on success' do
20
+ subject.parse(contents).should be_a Hash
21
+ end
22
+
23
+ it 'bubbles any exceptions thrown by the Parser' do
24
+ lambda { subject.parse('garbage') }.should raise_error
25
+ end
26
+
27
+ it 'returns nil on error when safe is set to true' do
28
+ subject.parse('garbage', true).should be_nil
29
+ end
30
+
31
+ end
32
+
33
+ context '#load' do
34
+
35
+ it 'throws an exception if the file does not exist' do
36
+ lambda { subject.load(filename) }.should raise_error
37
+ end
38
+
39
+ it 'throws an exception on inadequate file permissions' do
40
+ #pending 'FakeFS is not properly setting file permissions'
41
+ #write_ini_file
42
+ #FileUtils.chmod(0222, filename)
43
+ File.stub(:open).and_raise(Errno::EACCES)
44
+ lambda { subject.load(filename) }.should raise_error
45
+ end
46
+
47
+ it 'passes the file contents to the Parser' do
48
+ write_ini_file
49
+ IniFile::Parser.should_receive(:parse).with(contents)
50
+ subject.load(filename)
51
+ end
52
+
53
+ it 'expands the full file path' do
54
+ write_ini_file(ENV['HOME'])
55
+ lambda { subject.load(File.join("~/#{filename}")) }.should_not raise_error
56
+ end
57
+
58
+ it 'bubbles any exceptions thrown by the Content parser' do
59
+ File.open(filename, 'w') {|f| f.write(contents) }
60
+ IniFile::Content.stub(:parse).with(any_args()).and_raise(StandardError.new('test exception'))
61
+ lambda { subject.load(filename) }.should raise_error(StandardError)
62
+ end
63
+
64
+ it 'returns the new contents object' do
65
+ File.open(filename, 'w') {|f| f.write(contents) }
66
+ IniFile.load(filename).should be_instance_of IniFile::Content
67
+ end
68
+
69
+ it 'returns nil on error when safe is set to true' do
70
+ subject.load('file that does not exist', true).should be_nil
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,12 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'ini_file'
6
+
7
+ require 'rspec'
8
+ require 'fakefs'
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
data/tasks/rcov.rake ADDED
@@ -0,0 +1,11 @@
1
+ namespace :rcov do
2
+
3
+ RSpec::Core::RakeTask.new(:rspec) do |t|
4
+ rm "coverage.data" if File.exist?("coverage.data")
5
+ t.pattern = 'spec/**/*_spec.rb'
6
+ t.rcov = true
7
+ t.rcov_opts = %w{--exclude osx\/objc,Users\/,gems\/,spec\/,teamcity}
8
+ t.verbose = true
9
+ end
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ini_file
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jerry D'Antonio
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: debugger
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: simplecov
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: fakefs
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: ! '''The INI file format is an informal standard for configuration files
111
+ for some platforms or software. INI files are simple text files with a basic structure
112
+ composed of ''sections'' and ''properties''.'' - Wikipedia'
113
+ email:
114
+ - jerry.dantonio@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - Rakefile
120
+ - README.rdoc
121
+ - LICENSE
122
+ - lib/ini_file.rb
123
+ - lib/ini_file/version.rb
124
+ - lib/ini_file/parser.rb
125
+ - lib/ini_file/content.rb
126
+ - spec/ini_file_spec.rb
127
+ - spec/spec_helper.rb
128
+ - spec/ini_file/content_spec.rb
129
+ - spec/ini_file/parser_spec.rb
130
+ - tasks/rcov.rake
131
+ homepage: https://github.com/jdantonio/ini_file
132
+ licenses: []
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ - lib/ini_file
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ! '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 1.8.24
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Manage simple INI files.
156
+ test_files:
157
+ - spec/ini_file_spec.rb
158
+ - spec/spec_helper.rb
159
+ - spec/ini_file/content_spec.rb
160
+ - spec/ini_file/parser_spec.rb