inifile 2.0.2 → 3.0.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/.gitignore +1 -0
- data/README.md +23 -1
- data/Rakefile +1 -1
- data/inifile.gemspec +24 -0
- data/lib/inifile.rb +277 -201
- data/test/data/bad_2.ini +11 -0
- data/test/data/comment.ini +1 -1
- data/test/data/continuation.ini +6 -0
- data/test/data/good.ini +4 -4
- data/test/data/section.ini +4 -0
- data/test/test_inifile.rb +132 -63
- metadata +65 -41
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1b35f40f3c6c004a85d1b6b43766951b49a301e9
|
4
|
+
data.tar.gz: 789bc7312e534942372b41591b2099bf80508b46
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2aa8846aa49b6557fd860f66557b78a9a02bea56d3f5bb2e7b8bec2577a2557eb172464a618dee4d32fe5dde70c8453cbe67f71a3c6da4528ea3ba40317afb70
|
7
|
+
data.tar.gz: 73e1a6df1802d34439e0ec2924ab84511b58190f64b13072d734bd90fee048224e35ce378c678d9b8a76805e5d51066fd00ab1d877dbc89fd7349b5885451551
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -135,6 +135,19 @@ sequences to appear literally in your value. For example:
|
|
135
135
|
|
136
136
|
property = this is not a tab \\t character
|
137
137
|
|
138
|
+
### Value Type Casting
|
139
|
+
|
140
|
+
Some values will be type cast when parsed by the code. Those values are
|
141
|
+
booleans, integers, floats, and empty strings are cast to `nil`.
|
142
|
+
|
143
|
+
* "" --> nil
|
144
|
+
* "42" --> 42
|
145
|
+
* "3.14159" --> 3.14159
|
146
|
+
* "true" --> true
|
147
|
+
* "false" --> false
|
148
|
+
* "normal string" --> "normal string"
|
149
|
+
|
150
|
+
Pretty basic stuff.
|
138
151
|
|
139
152
|
Install
|
140
153
|
-------
|
@@ -150,6 +163,15 @@ To run the tests:
|
|
150
163
|
$ rake
|
151
164
|
|
152
165
|
|
166
|
+
Examples
|
167
|
+
--------
|
168
|
+
|
169
|
+
require 'inifile'
|
170
|
+
myini = IniFile.load('mytest.ini')
|
171
|
+
myini.each_section do |section|
|
172
|
+
puts "I want #{myini[section]['somevar']} printed here!"
|
173
|
+
end
|
174
|
+
|
153
175
|
Contributing
|
154
176
|
------------
|
155
177
|
|
@@ -171,7 +193,7 @@ License
|
|
171
193
|
-------
|
172
194
|
|
173
195
|
MIT License
|
174
|
-
Copyright (c) 2006 -
|
196
|
+
Copyright (c) 2006 - 2014
|
175
197
|
|
176
198
|
Permission is hereby granted, free of charge, to any person obtaining
|
177
199
|
a copy of this software and associated documentation files (the
|
data/Rakefile
CHANGED
data/inifile.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'inifile'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "inifile"
|
8
|
+
spec.version = IniFile::VERSION
|
9
|
+
spec.authors = ["Tim Pease"]
|
10
|
+
spec.email = ["tim.pease@gmail.com"]
|
11
|
+
spec.summary = %q{INI file reader and writer}
|
12
|
+
spec.description = %q{INI file reader and writer}
|
13
|
+
spec.description = %q{IniFile is a pure ruby gem that can read and write most INI file formats}
|
14
|
+
spec.homepage = "https://rubygems.org/gems/inifile"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bones", "~> 3.8"
|
23
|
+
spec.add_development_dependency "bones-git", "~> 1.3"
|
24
|
+
end
|
data/lib/inifile.rb
CHANGED
@@ -1,23 +1,20 @@
|
|
1
1
|
#encoding: UTF-8
|
2
|
-
require 'strscan'
|
3
2
|
|
4
3
|
# This class represents the INI file and can be used to parse, modify,
|
5
4
|
# and write INI files.
|
6
|
-
#
|
7
5
|
class IniFile
|
8
6
|
include Enumerable
|
9
7
|
|
10
8
|
class Error < StandardError; end
|
11
|
-
VERSION = '
|
9
|
+
VERSION = '3.0.0'
|
12
10
|
|
13
11
|
# Public: Open an INI file and load the contents.
|
14
12
|
#
|
15
|
-
# filename - The name of the
|
13
|
+
# filename - The name of the file as a String
|
16
14
|
# opts - The Hash of options (default: {})
|
17
15
|
# :comment - String containing the comment character(s)
|
18
16
|
# :parameter - String used to separate parameter and value
|
19
|
-
# :encoding - Encoding String for reading / writing
|
20
|
-
# :escape - Boolean used to control character escaping
|
17
|
+
# :encoding - Encoding String for reading / writing
|
21
18
|
# :default - The String name of the default global section
|
22
19
|
#
|
23
20
|
# Examples
|
@@ -28,8 +25,7 @@ class IniFile
|
|
28
25
|
# IniFile.load('does/not/exist.ini')
|
29
26
|
# #=> nil
|
30
27
|
#
|
31
|
-
# Returns an IniFile
|
32
|
-
#
|
28
|
+
# Returns an IniFile instance or nil if the file could not be opened.
|
33
29
|
def self.load( filename, opts = {} )
|
34
30
|
return unless File.file? filename
|
35
31
|
new(opts.merge(:filename => filename))
|
@@ -38,57 +34,50 @@ class IniFile
|
|
38
34
|
# Get and set the filename
|
39
35
|
attr_accessor :filename
|
40
36
|
|
41
|
-
# Get and set the encoding
|
37
|
+
# Get and set the encoding
|
42
38
|
attr_accessor :encoding
|
43
39
|
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
#
|
49
|
-
# :filename option is used to read in the content of the INI file. If
|
50
|
-
# neither the content for a filename is provided then an empty INI file is
|
51
|
-
# created.
|
40
|
+
# Public: Create a new INI file from the given set of options. If :content
|
41
|
+
# is provided then it will be used to populate the INI file. If a :filename
|
42
|
+
# is provided then the contents of the file will be parsed and stored in the
|
43
|
+
# INI file. If neither the :content or :filename is provided then an empty
|
44
|
+
# INI file is created.
|
52
45
|
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
# :filename - The filename as a String
|
46
|
+
# opts - The Hash of options (default: {})
|
47
|
+
# :content - The String/Hash containing the INI contents
|
48
|
+
# :comment - String containing the comment character(s)
|
49
|
+
# :parameter - String used to separate parameter and value
|
50
|
+
# :encoding - Encoding String for reading / writing
|
51
|
+
# :default - The String name of the default global section
|
52
|
+
# :filename - The filename as a String
|
61
53
|
#
|
62
54
|
# Examples
|
63
55
|
#
|
64
56
|
# IniFile.new
|
65
57
|
# #=> an empty IniFile instance
|
66
58
|
#
|
67
|
-
# IniFile.new( "[global]\nfoo=bar" )
|
59
|
+
# IniFile.new( :content => "[global]\nfoo=bar" )
|
68
60
|
# #=> an IniFile instance
|
69
61
|
#
|
70
62
|
# IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' )
|
71
63
|
# #=> an IniFile instance
|
72
64
|
#
|
73
|
-
# IniFile.new( "[global]\nfoo=bar", :comment => '#' )
|
65
|
+
# IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' )
|
74
66
|
# #=> an IniFile instance
|
75
67
|
#
|
76
|
-
def initialize(
|
77
|
-
opts, content = content, nil if Hash === content
|
78
|
-
|
79
|
-
@content = content
|
80
|
-
|
68
|
+
def initialize( opts = {} )
|
81
69
|
@comment = opts.fetch(:comment, ';#')
|
82
70
|
@param = opts.fetch(:parameter, '=')
|
83
71
|
@encoding = opts.fetch(:encoding, nil)
|
84
|
-
@escape = opts.fetch(:escape, true)
|
85
72
|
@default = opts.fetch(:default, 'global')
|
86
73
|
@filename = opts.fetch(:filename, nil)
|
74
|
+
content = opts.fetch(:content, nil)
|
87
75
|
|
88
76
|
@ini = Hash.new {|h,k| h[k] = Hash.new}
|
89
77
|
|
90
|
-
if
|
91
|
-
elsif
|
78
|
+
if content.is_a?(Hash) then merge!(content)
|
79
|
+
elsif content then parse(content)
|
80
|
+
elsif @filename then read
|
92
81
|
end
|
93
82
|
end
|
94
83
|
|
@@ -98,16 +87,13 @@ class IniFile
|
|
98
87
|
#
|
99
88
|
# opts - The default options Hash
|
100
89
|
# :filename - The filename as a String
|
101
|
-
# :encoding - The encoding as a String
|
90
|
+
# :encoding - The encoding as a String
|
102
91
|
#
|
103
92
|
# Returns this IniFile instance.
|
104
|
-
#
|
105
93
|
def write( opts = {} )
|
106
94
|
filename = opts.fetch(:filename, @filename)
|
107
95
|
encoding = opts.fetch(:encoding, @encoding)
|
108
|
-
mode =
|
109
|
-
"w:#{encoding.to_s}" :
|
110
|
-
'w'
|
96
|
+
mode = encoding ? "w:#{encoding}" : "w"
|
111
97
|
|
112
98
|
File.open(filename, mode) do |f|
|
113
99
|
@ini.each do |section,hash|
|
@@ -129,31 +115,22 @@ class IniFile
|
|
129
115
|
#
|
130
116
|
# opts - The default options Hash
|
131
117
|
# :filename - The filename as a String
|
132
|
-
# :encoding - The encoding as a String
|
118
|
+
# :encoding - The encoding as a String
|
133
119
|
#
|
134
120
|
# Returns this IniFile instance if the read was successful; nil is returned
|
135
121
|
# if the file could not be read.
|
136
|
-
#
|
137
122
|
def read( opts = {} )
|
138
123
|
filename = opts.fetch(:filename, @filename)
|
139
124
|
encoding = opts.fetch(:encoding, @encoding)
|
140
125
|
return unless File.file? filename
|
141
126
|
|
142
|
-
mode =
|
143
|
-
|
144
|
-
'r'
|
145
|
-
fd = File.open(filename, mode)
|
146
|
-
@content = fd.read
|
147
|
-
|
148
|
-
parse!
|
127
|
+
mode = encoding ? "r:#{encoding}" : "r"
|
128
|
+
File.open(filename, mode) { |fd| parse fd }
|
149
129
|
self
|
150
|
-
ensure
|
151
|
-
fd.close if fd && !fd.closed?
|
152
130
|
end
|
153
131
|
alias :restore :read
|
154
132
|
|
155
133
|
# Returns this IniFile converted to a String.
|
156
|
-
#
|
157
134
|
def to_s
|
158
135
|
s = []
|
159
136
|
@ini.each do |section,hash|
|
@@ -165,7 +142,6 @@ class IniFile
|
|
165
142
|
end
|
166
143
|
|
167
144
|
# Returns this IniFile converted to a Hash.
|
168
|
-
#
|
169
145
|
def to_h
|
170
146
|
@ini.dup
|
171
147
|
end
|
@@ -176,33 +152,50 @@ class IniFile
|
|
176
152
|
# other - The other IniFile.
|
177
153
|
#
|
178
154
|
# Returns a new IniFile.
|
179
|
-
#
|
180
155
|
def merge( other )
|
181
156
|
self.dup.merge!(other)
|
182
157
|
end
|
183
158
|
|
184
159
|
# Public: Merges other_inifile into this inifile, overwriting existing
|
185
|
-
# entries. Useful for having a system inifile with user
|
160
|
+
# entries. Useful for having a system inifile with user overridable settings
|
186
161
|
# elsewhere.
|
187
162
|
#
|
188
163
|
# other - The other IniFile.
|
189
164
|
#
|
190
165
|
# Returns this IniFile.
|
191
|
-
#
|
192
166
|
def merge!( other )
|
167
|
+
return self if other.nil?
|
168
|
+
|
193
169
|
my_keys = @ini.keys
|
194
|
-
other_keys =
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
170
|
+
other_keys = case other
|
171
|
+
when IniFile
|
172
|
+
other.instance_variable_get(:@ini).keys
|
173
|
+
when Hash
|
174
|
+
other.keys
|
175
|
+
else
|
176
|
+
raise Error, "cannot merge contents from '#{other.class.name}'"
|
177
|
+
end
|
199
178
|
|
200
179
|
(my_keys & other_keys).each do |key|
|
201
|
-
|
180
|
+
case other[key]
|
181
|
+
when Hash
|
182
|
+
@ini[key].merge!(other[key])
|
183
|
+
when nil
|
184
|
+
nil
|
185
|
+
else
|
186
|
+
raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
|
187
|
+
end
|
202
188
|
end
|
203
189
|
|
204
190
|
(other_keys - my_keys).each do |key|
|
205
|
-
@ini[key] = other[key]
|
191
|
+
@ini[key] = case other[key]
|
192
|
+
when Hash
|
193
|
+
other[key].dup
|
194
|
+
when nil
|
195
|
+
{}
|
196
|
+
else
|
197
|
+
raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
|
198
|
+
end
|
206
199
|
end
|
207
200
|
|
208
201
|
self
|
@@ -212,7 +205,7 @@ class IniFile
|
|
212
205
|
# given block.
|
213
206
|
#
|
214
207
|
# block - The block that will be iterated by the each method. The block will
|
215
|
-
# be passed the current section and the parameter
|
208
|
+
# be passed the current section and the parameter/value pair.
|
216
209
|
#
|
217
210
|
# Examples
|
218
211
|
#
|
@@ -221,7 +214,6 @@ class IniFile
|
|
221
214
|
# end
|
222
215
|
#
|
223
216
|
# Returns this IniFile.
|
224
|
-
#
|
225
217
|
def each
|
226
218
|
return unless block_given?
|
227
219
|
@ini.each do |section,hash|
|
@@ -244,7 +236,6 @@ class IniFile
|
|
244
236
|
# end
|
245
237
|
#
|
246
238
|
# Returns this IniFile.
|
247
|
-
#
|
248
239
|
def each_section
|
249
240
|
return unless block_given?
|
250
241
|
@ini.each_key {|section| yield section}
|
@@ -256,7 +247,6 @@ class IniFile
|
|
256
247
|
# section - The section name as a String.
|
257
248
|
#
|
258
249
|
# Returns the deleted section Hash.
|
259
|
-
#
|
260
250
|
def delete_section( section )
|
261
251
|
@ini.delete section.to_s
|
262
252
|
end
|
@@ -272,7 +262,6 @@ class IniFile
|
|
272
262
|
# #=> global section Hash
|
273
263
|
#
|
274
264
|
# Returns the Hash of parameter/value pairs for this section.
|
275
|
-
#
|
276
265
|
def []( section )
|
277
266
|
return nil if section.nil?
|
278
267
|
@ini[section.to_s]
|
@@ -289,7 +278,6 @@ class IniFile
|
|
289
278
|
# #=> { 'gritty' => 'yes' }
|
290
279
|
#
|
291
280
|
# Returns the value Hash.
|
292
|
-
#
|
293
281
|
def []=( section, value )
|
294
282
|
@ini[section.to_s] = value
|
295
283
|
end
|
@@ -306,7 +294,6 @@ class IniFile
|
|
306
294
|
#
|
307
295
|
# Return a Hash containing only those sections that match the given regular
|
308
296
|
# expression.
|
309
|
-
#
|
310
297
|
def match( regex )
|
311
298
|
@ini.dup.delete_if { |section, _| section !~ regex }
|
312
299
|
end
|
@@ -316,13 +303,11 @@ class IniFile
|
|
316
303
|
# section - The section name as a String.
|
317
304
|
#
|
318
305
|
# Returns true if the section exists in the IniFile.
|
319
|
-
#
|
320
306
|
def has_section?( section )
|
321
307
|
@ini.has_key? section.to_s
|
322
308
|
end
|
323
309
|
|
324
310
|
# Returns an Array of section names contained in this IniFile.
|
325
|
-
#
|
326
311
|
def sections
|
327
312
|
@ini.keys
|
328
313
|
end
|
@@ -331,7 +316,6 @@ class IniFile
|
|
331
316
|
# the object will raise an error.
|
332
317
|
#
|
333
318
|
# Returns this IniFile.
|
334
|
-
#
|
335
319
|
def freeze
|
336
320
|
super
|
337
321
|
@ini.each_value {|h| h.freeze}
|
@@ -343,7 +327,6 @@ class IniFile
|
|
343
327
|
# marking each as tainted.
|
344
328
|
#
|
345
329
|
# Returns this IniFile.
|
346
|
-
#
|
347
330
|
def taint
|
348
331
|
super
|
349
332
|
@ini.each_value {|h| h.taint}
|
@@ -356,7 +339,6 @@ class IniFile
|
|
356
339
|
# original. The tainted state of the original is copied to the duplicate.
|
357
340
|
#
|
358
341
|
# Returns a new IniFile.
|
359
|
-
#
|
360
342
|
def dup
|
361
343
|
other = super
|
362
344
|
other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new})
|
@@ -371,7 +353,6 @@ class IniFile
|
|
371
353
|
# to the duplicate.
|
372
354
|
#
|
373
355
|
# Returns a new IniFile.
|
374
|
-
#
|
375
356
|
def clone
|
376
357
|
other = dup
|
377
358
|
other.freeze if self.frozen?
|
@@ -385,7 +366,6 @@ class IniFile
|
|
385
366
|
# other - The other IniFile.
|
386
367
|
#
|
387
368
|
# Returns true if the INI files are equivalent and false if they differ.
|
388
|
-
#
|
389
369
|
def eql?( other )
|
390
370
|
return true if equal? other
|
391
371
|
return false unless other.instance_of? self.class
|
@@ -393,159 +373,255 @@ class IniFile
|
|
393
373
|
end
|
394
374
|
alias :== :eql?
|
395
375
|
|
396
|
-
|
397
|
-
private
|
398
|
-
|
399
|
-
# Parse the ini file contents. This will clear any values currently stored
|
400
|
-
# in the ini hash.
|
376
|
+
# Escape special characters.
|
401
377
|
#
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
378
|
+
# value - The String value to escape.
|
379
|
+
#
|
380
|
+
# Returns the escaped value.
|
381
|
+
def escape_value( value )
|
382
|
+
value = value.to_s.dup
|
383
|
+
value.gsub!(%r/\\([0nrt])/, '\\\\\1')
|
384
|
+
value.gsub!(%r/\n/, '\n')
|
385
|
+
value.gsub!(%r/\r/, '\r')
|
386
|
+
value.gsub!(%r/\t/, '\t')
|
387
|
+
value.gsub!(%r/\0/, '\0')
|
388
|
+
value
|
389
|
+
end
|
407
390
|
|
408
|
-
|
409
|
-
|
410
|
-
|
391
|
+
# Parse the given content and store the information in this IniFile
|
392
|
+
# instance. All data will be cleared out and replaced with the information
|
393
|
+
# read from the content.
|
394
|
+
#
|
395
|
+
# content - A String or a file descriptor (must respond to `each_line`)
|
396
|
+
#
|
397
|
+
# Returns this IniFile.
|
398
|
+
def parse( content )
|
399
|
+
parser = Parser.new(@ini, @param, @comment, @default)
|
400
|
+
parser.parse(content)
|
401
|
+
self
|
402
|
+
end
|
411
403
|
|
412
|
-
|
413
|
-
|
404
|
+
# The IniFile::Parser has the responsibility of reading the contents of an
|
405
|
+
# .ini file and storing that information into a ruby Hash. The object being
|
406
|
+
# parsed must respond to `each_line` - this includes Strings and any IO
|
407
|
+
# object.
|
408
|
+
class Parser
|
409
|
+
|
410
|
+
attr_writer :section
|
411
|
+
attr_accessor :property
|
412
|
+
attr_accessor :value
|
413
|
+
|
414
|
+
# Create a new IniFile::Parser that can be used to parse the contents of
|
415
|
+
# an .ini file.
|
416
|
+
#
|
417
|
+
# hash - The Hash where parsed information will be stored
|
418
|
+
# param - String used to separate parameter and value
|
419
|
+
# comment - String containing the comment character(s)
|
420
|
+
# default - The String name of the default global section
|
421
|
+
#
|
422
|
+
def initialize( hash, param, comment, default )
|
423
|
+
@hash = hash
|
424
|
+
@default = default
|
425
|
+
|
426
|
+
comment = comment.to_s.empty? ? "\\z" : "\\s*(?:[#{comment}].*)?\\z"
|
427
|
+
|
428
|
+
@section_regexp = %r/\A\s*\[([^\]]+)\]#{comment}/
|
429
|
+
@ignore_regexp = %r/\A#{comment}/
|
430
|
+
@property_regexp = %r/\A(.*?)(?<!\\)#{param}(.*)\z/
|
431
|
+
|
432
|
+
@open_quote = %r/\A\s*(".*)\z/
|
433
|
+
@close_quote = %r/\A(.*(?<!\\)")#{comment}/
|
434
|
+
@full_quote = %r/\A\s*(".*(?<!\\)")#{comment}/
|
435
|
+
@trailing_slash = %r/\A(.*)(?<!\\)\\#{comment}/
|
436
|
+
@normal_value = %r/\A(.*?)#{comment}/
|
437
|
+
end
|
414
438
|
|
415
|
-
|
416
|
-
|
439
|
+
# Returns `true` if the current value starts with a leading double quote.
|
440
|
+
# Otherwise returns false.
|
441
|
+
def leading_quote?
|
442
|
+
value && value =~ %r/\A"/
|
443
|
+
end
|
417
444
|
|
418
|
-
|
419
|
-
|
420
|
-
|
445
|
+
# Given a string, attempt to parse out a value from that string. This
|
446
|
+
# value might be continued on the following line. So this method returns
|
447
|
+
# `true` if it is expecting more data.
|
448
|
+
#
|
449
|
+
# string - String to parse
|
450
|
+
#
|
451
|
+
# Returns `true` if the next line is also part of the current value.
|
452
|
+
# Returns `fase` if the string contained a complete value.
|
453
|
+
def parse_value( string )
|
454
|
+
continuation = false
|
455
|
+
|
456
|
+
# if our value starts with a double quote, then we are in a
|
457
|
+
# line continuation situation
|
458
|
+
if leading_quote?
|
459
|
+
# check for a closing quote at the end of the string
|
460
|
+
if string =~ @close_quote
|
461
|
+
value << $1
|
462
|
+
|
463
|
+
# otherwise just append the string to the value
|
464
|
+
else
|
465
|
+
value << string
|
466
|
+
continuation = true
|
467
|
+
end
|
421
468
|
|
422
|
-
#
|
423
|
-
|
424
|
-
|
425
|
-
|
469
|
+
# not currently processing a continuation line
|
470
|
+
else
|
471
|
+
case string
|
472
|
+
when @full_quote
|
473
|
+
self.value = $1
|
426
474
|
|
427
|
-
|
428
|
-
|
475
|
+
when @open_quote
|
476
|
+
self.value = $1
|
477
|
+
continuation = true
|
429
478
|
|
430
|
-
|
431
|
-
|
432
|
-
|
479
|
+
when @trailing_slash
|
480
|
+
self.value ? self.value << $1 : self.value = $1
|
481
|
+
continuation = true
|
433
482
|
|
434
|
-
|
483
|
+
when @normal_value
|
484
|
+
self.value ? self.value << $1 : self.value = $1
|
435
485
|
|
436
|
-
# look for the separator between property name and value
|
437
|
-
elsif scanner.scan(%r/#{@param}/)
|
438
|
-
if property.empty?
|
439
|
-
property = string.strip
|
440
|
-
string.slice!(0, string.length)
|
441
486
|
else
|
442
|
-
|
487
|
+
error
|
443
488
|
end
|
489
|
+
end
|
444
490
|
|
445
|
-
|
446
|
-
|
447
|
-
@_section = @ini[scanner[1]]
|
448
|
-
|
449
|
-
# otherwise scan and store characters till we hit the start of some
|
450
|
-
# special section like a quote, newline, comment, etc.
|
491
|
+
if continuation
|
492
|
+
self.value << $/ if leading_quote?
|
451
493
|
else
|
452
|
-
|
453
|
-
parse_error if tmp.nil?
|
454
|
-
|
455
|
-
len = scanner[1].length
|
456
|
-
tmp.slice!(tmp.length - len, len)
|
457
|
-
|
458
|
-
scanner.pos = scanner.pos - len
|
459
|
-
string << tmp
|
494
|
+
process_property
|
460
495
|
end
|
496
|
+
|
497
|
+
continuation
|
461
498
|
end
|
462
499
|
|
463
|
-
|
464
|
-
|
500
|
+
# Parse the ini file contents. This will clear any values currently stored
|
501
|
+
# in the ini hash.
|
502
|
+
#
|
503
|
+
# content - Any object that responds to `each_line`
|
504
|
+
#
|
505
|
+
# Returns nil.
|
506
|
+
def parse( content )
|
507
|
+
return unless content
|
465
508
|
|
466
|
-
|
467
|
-
# method checks for continuation of the value to the next line.
|
468
|
-
#
|
469
|
-
# property - The property name as a String.
|
470
|
-
# value - The property value as a String.
|
471
|
-
#
|
472
|
-
# Returns nil.
|
473
|
-
#
|
474
|
-
def process_property( property, value )
|
475
|
-
value.chomp!
|
476
|
-
return if property.empty? and value.empty?
|
477
|
-
return if value.sub!(%r/\\\s*\z/, '')
|
509
|
+
continuation = false
|
478
510
|
|
479
|
-
|
480
|
-
|
511
|
+
@hash.clear
|
512
|
+
@line = nil
|
513
|
+
self.section = nil
|
481
514
|
|
482
|
-
|
515
|
+
content.each_line do |line|
|
516
|
+
@line = line.chomp
|
483
517
|
|
484
|
-
|
518
|
+
if continuation
|
519
|
+
continuation = parse_value @line
|
520
|
+
else
|
521
|
+
case @line
|
522
|
+
when @ignore_regexp
|
523
|
+
nil
|
524
|
+
when @section_regexp
|
525
|
+
self.section = @hash[$1]
|
526
|
+
when @property_regexp
|
527
|
+
self.property = $1.strip
|
528
|
+
error if property.empty?
|
529
|
+
|
530
|
+
continuation = parse_value $2
|
531
|
+
else
|
532
|
+
error
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
485
536
|
|
486
|
-
|
487
|
-
|
537
|
+
# check here if we have a dangling value ... usually means we have an
|
538
|
+
# unmatched open quote
|
539
|
+
if leading_quote?
|
540
|
+
error "Unmatched open quote"
|
541
|
+
elsif property && value
|
542
|
+
process_property
|
543
|
+
elsif value
|
544
|
+
error
|
545
|
+
end
|
488
546
|
|
489
|
-
|
490
|
-
|
547
|
+
nil
|
548
|
+
end
|
491
549
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
550
|
+
# Store the property/value pair in the currently active section. This
|
551
|
+
# method checks for continuation of the value to the next line.
|
552
|
+
#
|
553
|
+
# Returns nil.
|
554
|
+
def process_property
|
555
|
+
property.strip!
|
556
|
+
value.strip!
|
497
557
|
|
498
|
-
|
499
|
-
# being parsed.
|
500
|
-
#
|
501
|
-
# msg - The message String to use.
|
502
|
-
#
|
503
|
-
# Raises IniFile::Error
|
504
|
-
#
|
505
|
-
def parse_error( msg = 'Could not parse line' )
|
506
|
-
raise Error, "#{msg}: #{@_line.inspect}"
|
507
|
-
end
|
558
|
+
self.value = $1 if value =~ %r/\A"(.*)(?<!\\)"\z/m
|
508
559
|
|
509
|
-
|
510
|
-
# escaped null, tab, carriage return, newline, and backslash into their
|
511
|
-
# literal equivalents.
|
512
|
-
#
|
513
|
-
# value - The String value to unescape.
|
514
|
-
#
|
515
|
-
# Returns the unescaped value.
|
516
|
-
#
|
517
|
-
def unescape_value( value )
|
518
|
-
return value unless @escape
|
560
|
+
section[property] = typecast(value)
|
519
561
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
when '\0'; "\0"
|
524
|
-
when '\n'; "\n"
|
525
|
-
when '\r'; "\r"
|
526
|
-
when '\t'; "\t"
|
527
|
-
when '\\\\'; "\\"
|
528
|
-
end
|
529
|
-
}
|
530
|
-
value
|
531
|
-
end
|
562
|
+
self.property = nil
|
563
|
+
self.value = nil
|
564
|
+
end
|
532
565
|
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
# Returns the escaped value.
|
538
|
-
#
|
539
|
-
def escape_value( value )
|
540
|
-
return value unless @escape
|
566
|
+
# Returns the current section Hash.
|
567
|
+
def section
|
568
|
+
@section ||= @hash[@default]
|
569
|
+
end
|
541
570
|
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
571
|
+
# Raise a parse error using the given message and appending the current line
|
572
|
+
# being parsed.
|
573
|
+
#
|
574
|
+
# msg - The message String to use.
|
575
|
+
#
|
576
|
+
# Raises IniFile::Error
|
577
|
+
def error( msg = 'Could not parse line' )
|
578
|
+
raise Error, "#{msg}: #{@line.inspect}"
|
579
|
+
end
|
580
|
+
|
581
|
+
# Attempt to typecast the value string. We are looking for boolean values,
|
582
|
+
# integers, floats, and empty strings. Below is how each gets cast, but it
|
583
|
+
# is pretty logical and straightforward.
|
584
|
+
#
|
585
|
+
# "true" --> true
|
586
|
+
# "false" --> false
|
587
|
+
# "" --> nil
|
588
|
+
# "42" --> 42
|
589
|
+
# "3.14" --> 3.14
|
590
|
+
# "foo" --> "foo"
|
591
|
+
#
|
592
|
+
# Returns the typecast value.
|
593
|
+
def typecast( value )
|
594
|
+
case value
|
595
|
+
when %r/\Atrue\z/i; true
|
596
|
+
when %r/\Afalse\z/i; false
|
597
|
+
when %r/\A\s*\z/i; nil
|
598
|
+
else
|
599
|
+
Integer(value) rescue \
|
600
|
+
Float(value) rescue \
|
601
|
+
unescape_value(value)
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
# Unescape special characters found in the value string. This will convert
|
606
|
+
# escaped null, tab, carriage return, newline, and backslash into their
|
607
|
+
# literal equivalents.
|
608
|
+
#
|
609
|
+
# value - The String value to unescape.
|
610
|
+
#
|
611
|
+
# Returns the unescaped value.
|
612
|
+
def unescape_value( value )
|
613
|
+
value = value.to_s
|
614
|
+
value.gsub!(%r/\\[0nrt\\]/) { |char|
|
615
|
+
case char
|
616
|
+
when '\0'; "\0"
|
617
|
+
when '\n'; "\n"
|
618
|
+
when '\r'; "\r"
|
619
|
+
when '\t'; "\t"
|
620
|
+
when '\\\\'; "\\"
|
621
|
+
end
|
622
|
+
}
|
623
|
+
value
|
624
|
+
end
|
549
625
|
end
|
550
626
|
|
551
627
|
end # IniFile
|