o-inifile 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f60e4b8e5246f6327f832017a37ad425a603cdc0214fe882a7cab21b6345197e
4
+ data.tar.gz: ca427a71f5c017bd951a086a46e83f1ab909a3e2b8f52c2254485120b861a345
5
+ SHA512:
6
+ metadata.gz: fc1a611372e22d90d4dbcdac29d93d65bc2e7b6d4a3437eac0490e2a9d086eb1619906671db2fa2af05446cdb564659ed28522f2d81bf7b0a5f5a4ced87caf07
7
+ data.tar.gz: 7cbd4400167dbbfbb1eb4a118d9d29b40299cd03d78ad352f36c6b85f31e331e55891c48a97ac87afb3c9528270e6c9c5bbab8f66365a6c6cdde18fa2a8a81ee
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ # The list of files that should be ignored by Mr Bones.
2
+ # Lines that start with '#' are comments.
3
+ #
4
+ # A .gitignore file can be used instead by setting it as the ignore
5
+ # file in your Rakefile:
6
+ #
7
+ # PROJ.ignore_file = '.gitignore'
8
+ #
9
+ # For a project with a C extension, the following would be a good set of
10
+ # exclude patterns (uncomment them if you want to use them):
11
+ # *.[oa]
12
+ *~
13
+ *.sw[op]
14
+ announcement.txt
15
+ coverage
16
+ doc
17
+ pkg
18
+ vendor
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+
3
+ before_install: "gem install bones"
4
+ install: "rake gem:install_dependencies"
5
+
6
+ script: "rake"
7
+
8
+ rvm:
9
+ - 1.9.3
10
+ - 2.0.0
11
+ - 2.1.1
12
+
data/History.txt ADDED
@@ -0,0 +1,59 @@
1
+ == 2.0.2 / 2012-09-14
2
+
3
+ Bug Fixes
4
+ - Lack of newline at end of file causes error (thanks erebor) [issue #12]
5
+
6
+ == 2.0.1 / 2012-09-06
7
+
8
+ Bug Fixes
9
+ - Better error message for malformed INI files
10
+
11
+ == 2.0.0 / 2012-08-29
12
+
13
+ Major Enhancements
14
+ - Can now initialize from Strings [issue #9]
15
+ - Quoted multi-line values are supported
16
+ - Comments can appear at the end of a line
17
+ Enhancements
18
+ - TomDoc documentation
19
+ - Parity between read/write methods
20
+
21
+ == 1.1.0 / 2012-02-28
22
+
23
+ Enhancements
24
+ - Added support for a default "global" section
25
+
26
+ == 1.0.0 / 2012-02-27
27
+
28
+ Major Enhancements
29
+ - Changed how multi-line values are handled [issue #7]
30
+ - Added backslash escaping support for values
31
+ Enhancements
32
+ - Inifile merge functionality [Jens Hilligsøe]
33
+ - Use regular expressions to find sections [issue 8]
34
+
35
+ == 0.4.1 / 2011-02-22
36
+
37
+ Bug Fixes
38
+ - Empty quotes [Steffen Beyer]
39
+
40
+ == 0.4.0 / 2011-02-15
41
+
42
+ Enhancements
43
+ - Added multiline support [André Gawron]
44
+ - Added file encoding support [Gilles Devaux]
45
+
46
+ == 0.3.0 / 2009-12-10
47
+
48
+ * 1 minor enhancement
49
+ - Added a []= operator to the inifile for setting entire sections
50
+
51
+ == 0.2.0 / 2009-10-27
52
+
53
+ * 1 minor enhancement
54
+ - Using Mr Bones for rake tasks and gem management
55
+
56
+ == 0.1.0 / 2006-11-26
57
+
58
+ * 1 major enhancement
59
+ * Birthday!
data/README.md ADDED
@@ -0,0 +1,243 @@
1
+ inifile
2
+ =======
3
+
4
+ This is a native Ruby library for reading and writing INI files.
5
+
6
+ This project is a fork of the [original inifile library](https://github.com/TwP/inifile).
7
+ by Tim Pease (TwP).
8
+
9
+
10
+ Description
11
+ -----------
12
+
13
+ Although made popular by Windows, INI files can be used on any system thanks
14
+ to their flexibility. They allow a program to store configuration data, which
15
+ can then be easily parsed and changed. Two notable systems that use the INI
16
+ format are Samba and Trac.
17
+
18
+ More information about INI files can be found on the [Wikipedia Page](http://en.wikipedia.org/wiki/INI_file).
19
+
20
+ ### Properties
21
+
22
+ A basic element contained in an INI file is the property. Every property has
23
+ a name and a value, delimited by an equals sign *=*. The name appears to the
24
+ left of the equals sign and the value to the right.
25
+
26
+ name=value
27
+
28
+ ### Switches
29
+
30
+ Switches are sometimes present in INI files to indicate whether a feature
31
+ should be on or off. Switches are made up by a single word on a line on its
32
+ own.
33
+
34
+ name
35
+
36
+ Switches are represented internally as an empty hash `{}` to avoid clashing
37
+ with other values and are deactivated by removing the line from the file
38
+ completely.
39
+
40
+ ### Sections
41
+
42
+ Section declarations start with *[* and end with *]* as in `[section1]` and
43
+ `[section2]` shown in the example below. The section declaration marks the
44
+ beginning of a section. All properties after the section declaration will be
45
+ associated with that section.
46
+
47
+ ### Comments
48
+
49
+ All lines beginning with a semicolon *;* or a number sign *#* are considered
50
+ to be comments. Comment lines are ignored when parsing INI files.
51
+
52
+ ### Example File Format
53
+
54
+ A typical INI file might look like this:
55
+
56
+ [section1]
57
+ ; some comment on section1
58
+ var1 = foo
59
+ var2 = doodle
60
+ var3 = multiline values \
61
+ are also possible
62
+
63
+ [section2]
64
+ # another comment
65
+ var1 = baz
66
+ var2 = shoodle
67
+ a_switch
68
+
69
+
70
+ Implementation
71
+ --------------
72
+
73
+ The format of INI files is not well defined. Several assumptions are made by
74
+ the **inifile** gem when parsing INI files. Most of these assumptions can be
75
+ modified at, but the defaults are listed below.
76
+
77
+ ### Global Properties
78
+
79
+ If the INI file lacks any section declarations, or if there are properties
80
+ decalared before the first section, then these properties will be placed into
81
+ a default "global" section. The name of this section can be configured when
82
+ creating an `IniFile` instance.
83
+
84
+ ### Duplicate Properties
85
+
86
+ Duplicate properties are allowed in a single section. The last property value
87
+ set is the one that will be stored in the `IniFile` instance.
88
+
89
+ [section1]
90
+ var1 = foo
91
+ var2 = bar
92
+ var1 = poodle
93
+
94
+ The resulting value of `var1` will be `poodle`.
95
+
96
+ ### Duplicate Sections
97
+
98
+ If you have more than one section with the same name then the sections will be
99
+ merged. Duplicate properties between the two sections will follow the rules
100
+ discussed above. Properties in the latter section will override properties in
101
+ the earlier section.
102
+
103
+ ### Comments
104
+
105
+ The comment character can be either a semicolon *;* or a number sign *#*. The
106
+ comment character can appear anywhere on a line including at the end of a
107
+ name/value pair declaration. If you wish to use a comment character in your
108
+ value then you will need to either escape the character or put the value in
109
+ double quotations.
110
+
111
+ [section1]
112
+ var1 = foo # a comment
113
+ var2 = "foo # this is not a comment"
114
+ var3 = foo \# this is not a comment either
115
+
116
+ ### Multi-Line Values
117
+
118
+ Values can be continued onto multiple lines in two separate ways. Putting a
119
+ slash at the end of a line will continue the value declaration to the next
120
+ line. When parsing, the trailing slash will be consumed and **will not**
121
+ appear in the resulting value. Comments can appear to the right of the
122
+ trailing slash.
123
+
124
+ var1 = this is a \ # these comments will
125
+ multiline value # be ignored by the parser
126
+
127
+ In the above example the resulting value for `var1` will be `this is a
128
+ multiline value`. If you want to preserve newline characters in the value then
129
+ quotations should be used.
130
+
131
+ var2 = "this is a
132
+ multiline value"
133
+
134
+ The resulting value for `var2` will be `this is a\nmultiline value`.
135
+
136
+ ### Escape Characters
137
+
138
+ Several escape characters are supported within the **value** for a property.
139
+ These escape sequences will be applied to quoted and unquoted values alike.
140
+ You can enable or disable escaping by setting the **escape** flag to true or
141
+ false when creating an IniFile instance.
142
+
143
+ * \0 -- null character
144
+ * \n -- newline character
145
+ * \r -- carriage return character
146
+ * \t -- tab character
147
+ * \\\\ -- backslash character
148
+
149
+ The backslash escape sequence is only needed if you want one of the escape
150
+ sequences to appear literally in your value. For example:
151
+
152
+ property = this is not a tab \\t character
153
+
154
+ ### Value Type Casting
155
+
156
+ Some values will be type cast when parsed by the code. Those values are
157
+ booleans, integers, floats, and empty strings are cast to `nil`.
158
+
159
+ * "" --> nil
160
+ * "42" --> 42
161
+ * "3.14159" --> 3.14159
162
+ * "true" --> true
163
+ * "false" --> false
164
+ * "normal string" --> "normal string"
165
+
166
+ Pretty basic stuff.
167
+
168
+ Install
169
+ -------
170
+
171
+ gem install inifile
172
+
173
+
174
+ Testing
175
+ -------
176
+
177
+ To run the tests:
178
+
179
+ $ rake
180
+
181
+
182
+ Examples
183
+ --------
184
+
185
+ Load from a file:
186
+
187
+ require 'inifile'
188
+ myini = IniFile.load('mytest.ini')
189
+ myini.each_section do |section|
190
+ puts "I want #{myini[section]['somevar']} printed here!"
191
+ end
192
+
193
+ Read INI content from a string:
194
+
195
+ myini = IniFile.new(:content => "[global]\nfoo=bar")
196
+
197
+ Create and save a new INI file:
198
+
199
+ myini = IniFile.new(filename: 'mytest.ini')
200
+ myini['section'] = {key: 'value'}
201
+ myini.write
202
+
203
+ Contributing
204
+ ------------
205
+
206
+ Contributions are gladly welcome! For small modifications (fixing typos,
207
+ improving documentation) you can use GitHub's in-browser editing capabilities
208
+ to create a pull request. For larger modifications I would recommend forking
209
+ the project, creating your patch, and then submitting a pull request.
210
+
211
+ Mr Bones is used to manage rake tasks and to install dependent files. To setup
212
+ your environment ...
213
+
214
+ $ gem install bones
215
+ $ rake gem:install_dependencies
216
+
217
+ And always remember that `rake -T` will show you the list of available tasks.
218
+
219
+
220
+ License
221
+ -------
222
+
223
+ MIT License
224
+ Copyright (c) 2006 - 2021
225
+
226
+ Permission is hereby granted, free of charge, to any person obtaining
227
+ a copy of this software and associated documentation files (the
228
+ 'Software'), to deal in the Software without restriction, including
229
+ without limitation the rights to use, copy, modify, merge, publish,
230
+ distribute, sublicense, and/or sell copies of the Software, and to
231
+ permit persons to whom the Software is furnished to do so, subject to
232
+ the following conditions:
233
+
234
+ The above copyright notice and this permission notice shall be
235
+ included in all copies or substantial portions of the Software.
236
+
237
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
238
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
239
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
240
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
241
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
242
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
243
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+
2
+ begin
3
+ require 'bones'
4
+ rescue LoadError
5
+ abort '### please install the "bones" gem ###'
6
+ end
7
+
8
+ ensure_in_path 'lib'
9
+ require 'inifile'
10
+
11
+ task :default => 'test:run'
12
+ task 'gem:release' => 'test:run'
13
+
14
+ Bones {
15
+ name 'o-inifile'
16
+ summary 'INI file reader and writer'
17
+ authors 'Oleg Pudeyev'
18
+ email 'code@olegp.name'
19
+ url 'https://github.com/p/o-inifile'
20
+ version IniFile::VERSION
21
+
22
+ use_gmail
23
+ depend_on 'bones-git', "~> 1.3", :development => true
24
+ }
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 = "o-inifile"
8
+ spec.version = IniFile::VERSION
9
+ spec.authors = ['Oleg Pudeyev', "Tim Pease"]
10
+ spec.email = ['code@olegp.name', "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://github.com/p/o-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 ADDED
@@ -0,0 +1,682 @@
1
+ #encoding: UTF-8
2
+
3
+ # This class represents the INI file and can be used to parse, modify,
4
+ # and write INI files.
5
+ class IniFile
6
+ include Enumerable
7
+
8
+ VERSION = '4.0.0'
9
+
10
+ class Error < StandardError; end
11
+
12
+ # Public: Open an INI file and load the contents.
13
+ #
14
+ # filename - The name of the file as a String
15
+ # opts - The Hash of options (default: {})
16
+ # :comment - String containing the comment character(s)
17
+ # :parameter - String used to separate parameter and value
18
+ # :encoding - Encoding String for reading / writing
19
+ # :default - The String name of the default global section
20
+ # :continuation - Use backslash as a line contintuation
21
+ #
22
+ # Examples
23
+ #
24
+ # IniFile.load('file.ini')
25
+ # #=> IniFile instance
26
+ #
27
+ # IniFile.load('does/not/exist.ini')
28
+ # #=> nil
29
+ #
30
+ # Returns an IniFile instance or nil if the file could not be opened.
31
+ def self.load( filename, opts = {} )
32
+ return unless File.file? filename
33
+ new(opts.merge(:filename => filename))
34
+ end
35
+
36
+ # Get and set the filename
37
+ attr_accessor :filename
38
+
39
+ # Get and set the encoding
40
+ attr_accessor :encoding
41
+
42
+ # Public: Create a new INI file from the given set of options. If :content
43
+ # is provided then it will be used to populate the INI file. If a :filename
44
+ # is provided then the contents of the file will be parsed and stored in the
45
+ # INI file. If neither the :content or :filename is provided then an empty
46
+ # INI file is created.
47
+ #
48
+ # opts - The Hash of options (default: {})
49
+ # :content - The String/Hash containing the INI contents
50
+ # :comment - String containing the comment character(s)
51
+ # :parameter - String used to separate parameter and value
52
+ # :encoding - Encoding String for reading / writing
53
+ # :default - The String name of the default global section
54
+ # :filename - The filename as a String
55
+ # :permissions - Permission bits to assign the new file
56
+ # :continuation - Use backslash as a line continuation
57
+ # :separator - what to output between the key, operator, and value
58
+ # :force_array - Keep all values with same key in an array
59
+ #
60
+ # Examples
61
+ #
62
+ # IniFile.new
63
+ # #=> an empty IniFile instance
64
+ #
65
+ # IniFile.new( :content => "[global]\nfoo=bar" )
66
+ # #=> an IniFile instance
67
+ #
68
+ # IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' )
69
+ # #=> an IniFile instance
70
+ #
71
+ # IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' )
72
+ # #=> an IniFile instance
73
+ #
74
+ # IniFile.new( :permissions => 0644 )
75
+ # #=> an IniFile instance
76
+ #
77
+ def initialize( opts = {} )
78
+ @comment = opts.fetch(:comment, ';#')
79
+ @param = opts.fetch(:parameter, '=')
80
+ @encoding = opts.fetch(:encoding, nil)
81
+ @default = opts.fetch(:default, 'global')
82
+ @filename = opts.fetch(:filename, nil)
83
+ @permissions = opts.fetch(:permissions, nil)
84
+ @continuation = opts.fetch(:continuation, true)
85
+ @separator = opts.fetch(:separator, ' ')
86
+ @force_array = opts.fetch(:force_array, nil)
87
+ content = opts.fetch(:content, nil)
88
+
89
+ @ini = Hash.new {|h,k| h[k] = Hash.new}
90
+
91
+ if content.is_a?(Hash) then merge!(content)
92
+ elsif content then parse(content)
93
+ elsif @filename then read
94
+ end
95
+ end
96
+
97
+ # Public: Write the contents of this IniFile to the file system. If left
98
+ # unspecified, the currently configured filename and encoding will be used.
99
+ # Otherwise the filename and encoding can be specified in the options hash.
100
+ #
101
+ # opts - The default options Hash
102
+ # :filename - The filename as a String
103
+ # :encoding - The encoding as a String
104
+ # :permissions - The permission bits as a Fixnum
105
+ #
106
+ # Returns this IniFile instance.
107
+ def write( opts = {} )
108
+ filename = opts.fetch(:filename, @filename)
109
+ encoding = opts.fetch(:encoding, @encoding)
110
+ permissions = opts.fetch(:permissions, @permissions)
111
+ mode = encoding ? "w:#{encoding}" : "w"
112
+
113
+ File.open(filename, mode, permissions) do |f|
114
+ @ini.each do |section,hash|
115
+ f.puts "[#{section}]"
116
+ hash.each {|param,val|
117
+ if val.class == Hash and val.empty?
118
+ f.puts param
119
+ else
120
+ if !val.is_a?(Array) || !@force_array
121
+ f.puts "#{param}#{@separator}#{@param}#{@separator}#{escape_value val}"
122
+ else
123
+ val.each do |subval|
124
+ f.puts "#{param}#{@separator}#{@param}#{@separator}#{escape_value subval}"
125
+ end
126
+ end
127
+ end
128
+ }
129
+ f.puts
130
+ end
131
+ end
132
+
133
+ self
134
+ end
135
+ alias :save :write
136
+
137
+ # Public: Read the contents of the INI file from the file system and replace
138
+ # and set the state of this IniFile instance. If left unspecified the
139
+ # currently configured filename and encoding will be used when reading from
140
+ # the file system. Otherwise the filename and encoding can be specified in
141
+ # the options hash.
142
+ #
143
+ # opts - The default options Hash
144
+ # :filename - The filename as a String
145
+ # :encoding - The encoding as a String
146
+ #
147
+ # Returns this IniFile instance if the read was successful; nil is returned
148
+ # if the file could not be read.
149
+ def read( opts = {} )
150
+ filename = opts.fetch(:filename, @filename)
151
+ encoding = opts.fetch(:encoding, @encoding)
152
+ return unless File.file? filename
153
+
154
+ mode = encoding ? "r:#{encoding}" : "r"
155
+ File.open(filename, mode) { |fd| parse fd }
156
+ self
157
+ end
158
+ alias :restore :read
159
+
160
+ # Returns this IniFile converted to a String.
161
+ def to_s
162
+ s = []
163
+ hash = @ini.dup
164
+ default = hash.delete(@default)
165
+ default.each {|param,val| s << "#{param} #{@param} #{escape_value val}"}
166
+ hash.each do |section,hash|
167
+ s << "[#{section}]"
168
+ hash.each {|param,val| s << "#{param}#{@separator}#{@param}#{@separator}#{escape_value val}"}
169
+ s << ""
170
+ end
171
+ s.join("\n")
172
+ end
173
+
174
+ # Returns this IniFile converted to a Hash.
175
+ def to_h
176
+ @ini.dup
177
+ end
178
+
179
+ # Public: Creates a copy of this inifile with the entries from the
180
+ # other_inifile merged into the copy.
181
+ #
182
+ # other - The other IniFile.
183
+ #
184
+ # Returns a new IniFile.
185
+ def merge( other )
186
+ self.dup.merge!(other)
187
+ end
188
+
189
+ # Public: Merges other_inifile into this inifile, overwriting existing
190
+ # entries. Useful for having a system inifile with user overridable settings
191
+ # elsewhere.
192
+ #
193
+ # other - The other IniFile.
194
+ #
195
+ # Returns this IniFile.
196
+ def merge!( other )
197
+ return self if other.nil?
198
+
199
+ my_keys = @ini.keys
200
+ other_keys = case other
201
+ when IniFile
202
+ other.instance_variable_get(:@ini).keys
203
+ when Hash
204
+ other.keys
205
+ else
206
+ raise Error, "cannot merge contents from '#{other.class.name}'"
207
+ end
208
+
209
+ (my_keys & other_keys).each do |key|
210
+ case other[key]
211
+ when Hash
212
+ @ini[key].merge!(other[key])
213
+ when nil
214
+ nil
215
+ else
216
+ raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
217
+ end
218
+ end
219
+
220
+ (other_keys - my_keys).each do |key|
221
+ case other[key]
222
+ when Hash
223
+ @ini[key] = other[key].dup
224
+ when nil
225
+ @ini[key] = {}
226
+ when String
227
+ @ini[@default].merge!({key => other[key]})
228
+ else
229
+ raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
230
+ end
231
+ end
232
+
233
+ self
234
+ end
235
+
236
+ # Public: Yield each INI file section, parameter, and value in turn to the
237
+ # given block.
238
+ #
239
+ # block - The block that will be iterated by the each method. The block will
240
+ # be passed the current section and the parameter/value pair.
241
+ #
242
+ # Examples
243
+ #
244
+ # inifile.each do |section, parameter, value|
245
+ # puts "#{parameter} = #{value} [in section - #{section}]"
246
+ # end
247
+ #
248
+ # Returns this IniFile.
249
+ def each
250
+ return unless block_given?
251
+ @ini.each do |section,hash|
252
+ hash.each do |param,val|
253
+ yield section, param, val
254
+ end
255
+ end
256
+ self
257
+ end
258
+
259
+ # Public: Yield each section in turn to the given block.
260
+ #
261
+ # block - The block that will be iterated by the each method. The block will
262
+ # be passed the current section as a Hash.
263
+ #
264
+ # Examples
265
+ #
266
+ # inifile.each_section do |section|
267
+ # puts section.inspect
268
+ # end
269
+ #
270
+ # Returns this IniFile.
271
+ def each_section
272
+ return unless block_given?
273
+ @ini.each_key {|section| yield section}
274
+ self
275
+ end
276
+
277
+ # Public: Remove a section identified by name from the IniFile.
278
+ #
279
+ # section - The section name as a String.
280
+ #
281
+ # Returns the deleted section Hash.
282
+ def delete_section( section )
283
+ @ini.delete section.to_s
284
+ end
285
+
286
+ # Public: Get the section Hash by name. If the section does not exist, then
287
+ # it will be created.
288
+ #
289
+ # section - The section name as a String.
290
+ #
291
+ # Examples
292
+ #
293
+ # inifile['global']
294
+ # #=> global section Hash
295
+ #
296
+ # Returns the Hash of parameter/value pairs for this section.
297
+ def []( section )
298
+ return nil if section.nil?
299
+ @ini[section.to_s]
300
+ end
301
+
302
+ # Public: Set the section to a hash of parameter/value pairs.
303
+ #
304
+ # section - The section name as a String.
305
+ # value - The Hash of parameter/value pairs.
306
+ #
307
+ # Examples
308
+ #
309
+ # inifile['tenderloin'] = { 'gritty' => 'yes' }
310
+ # #=> { 'gritty' => 'yes' }
311
+ #
312
+ # Returns the value Hash.
313
+ def []=( section, value )
314
+ @ini[section.to_s] = value
315
+ end
316
+
317
+ # Public: Create a Hash containing only those INI file sections whose names
318
+ # match the given regular expression.
319
+ #
320
+ # regex - The Regexp used to match section names.
321
+ #
322
+ # Examples
323
+ #
324
+ # inifile.match(/^tree_/)
325
+ # #=> Hash of matching sections
326
+ #
327
+ # Return a Hash containing only those sections that match the given regular
328
+ # expression.
329
+ def match( regex )
330
+ @ini.dup.delete_if { |section, _| section !~ regex }
331
+ end
332
+
333
+ # Public: Check to see if the IniFile contains the section.
334
+ #
335
+ # section - The section name as a String.
336
+ #
337
+ # Returns true if the section exists in the IniFile.
338
+ def has_section?( section )
339
+ @ini.has_key? section.to_s
340
+ end
341
+
342
+ # Returns an Array of section names contained in this IniFile.
343
+ def sections
344
+ @ini.keys
345
+ end
346
+
347
+ # Public: Freeze the state of this IniFile object. Any attempts to change
348
+ # the object will raise an error.
349
+ #
350
+ # Returns this IniFile.
351
+ def freeze
352
+ super
353
+ @ini.each_value {|h| h.freeze}
354
+ @ini.freeze
355
+ self
356
+ end
357
+
358
+ # Public: Mark this IniFile as tainted -- this will traverse each section
359
+ # marking each as tainted.
360
+ #
361
+ # Returns this IniFile.
362
+ def taint
363
+ super
364
+ @ini.each_value {|h| h.taint}
365
+ @ini.taint
366
+ self
367
+ end
368
+
369
+ # Public: Produces a duplicate of this IniFile. The duplicate is independent
370
+ # of the original -- i.e. the duplicate can be modified without changing the
371
+ # original. The tainted state of the original is copied to the duplicate.
372
+ #
373
+ # Returns a new IniFile.
374
+ def dup
375
+ other = super
376
+ other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new})
377
+ @ini.each_pair {|s,h| other[s].merge! h}
378
+ other.taint if self.tainted?
379
+ other
380
+ end
381
+
382
+ # Public: Produces a duplicate of this IniFile. The duplicate is independent
383
+ # of the original -- i.e. the duplicate can be modified without changing the
384
+ # original. The tainted state and the frozen state of the original is copied
385
+ # to the duplicate.
386
+ #
387
+ # Returns a new IniFile.
388
+ def clone
389
+ other = dup
390
+ other.freeze if self.frozen?
391
+ other
392
+ end
393
+
394
+ # Public: Compare this IniFile to some other IniFile. For two INI files to
395
+ # be equivalent, they must have the same sections with the same parameter /
396
+ # value pairs in each section.
397
+ #
398
+ # other - The other IniFile.
399
+ #
400
+ # Returns true if the INI files are equivalent and false if they differ.
401
+ def eql?( other )
402
+ return true if equal? other
403
+ return false unless other.instance_of? self.class
404
+ @ini == other.instance_variable_get(:@ini)
405
+ end
406
+ alias :== :eql?
407
+
408
+ # Escape special characters.
409
+ #
410
+ # value - The String value to escape.
411
+ #
412
+ # Returns the escaped value.
413
+ def escape_value( value )
414
+ value = value.to_s.dup
415
+ value.gsub!(%r/\\([0nrt])/, '\\\\\1')
416
+ value.gsub!(%r/\n/, '\n')
417
+ value.gsub!(%r/\r/, '\r')
418
+ value.gsub!(%r/\t/, '\t')
419
+ value.gsub!(%r/\0/, '\0')
420
+ value
421
+ end
422
+
423
+ # Parse the given content and store the information in this IniFile
424
+ # instance. All data will be cleared out and replaced with the information
425
+ # read from the content.
426
+ #
427
+ # content - A String or a file descriptor (must respond to `each_line`)
428
+ #
429
+ # Returns this IniFile.
430
+ def parse( content )
431
+ parser = Parser.new(@ini, @param, @comment, @default, @continuation, @force_array)
432
+ parser.parse(content)
433
+ self
434
+ end
435
+
436
+ # The IniFile::Parser has the responsibility of reading the contents of an
437
+ # .ini file and storing that information into a ruby Hash. The object being
438
+ # parsed must respond to `each_line` - this includes Strings and any IO
439
+ # object.
440
+ class Parser
441
+
442
+ attr_writer :section
443
+ attr_accessor :property
444
+ attr_accessor :value
445
+
446
+ # Create a new IniFile::Parser that can be used to parse the contents of
447
+ # an .ini file.
448
+ #
449
+ # hash - The Hash where parsed information will be stored
450
+ # param - String used to separate parameter and value
451
+ # comment - String containing the comment character(s)
452
+ # default - The String name of the default global section
453
+ # continuation - Use backslash as a line continuation character
454
+ #
455
+ def initialize( hash, param, comment, default, continuation, force_array )
456
+ @hash = hash
457
+ @default = default
458
+ @continuation = continuation
459
+ @force_array = force_array
460
+
461
+ comment = comment.to_s.empty? ? "\\z" : "\\s*(?:[#{comment}].*)?\\z"
462
+
463
+ @section_regexp = %r/\A\s*\[([^\]]+)\]#{comment}/
464
+ @ignore_regexp = %r/\A#{comment}/
465
+ @property_regexp = %r/\A(.*?)(?<!\\)#{param}(.*)\z/
466
+ @switch_regexp = %r/\A\s*([\w\.]+)\s*#{comment}/
467
+
468
+ @open_quote = %r/\A\s*(".*)\z/
469
+ @close_quote = %r/\A(.*(?<!\\)")#{comment}/
470
+ @full_quote = %r/\A\s*(".*(?<!\\)")#{comment}/
471
+ if @continuation
472
+ @trailing_slash = %r/\A(.*)(?<!\\)\\#{comment}/
473
+ else
474
+ @trailing_slash = %r/\A(.*\\)#{comment}/
475
+ end
476
+ @normal_value = %r/\A(.*?)#{comment}/
477
+ end
478
+
479
+ # Returns `true` if the current value starts with a leading double quote.
480
+ # Otherwise returns false.
481
+ def leading_quote?
482
+ value && value =~ %r/\A"/
483
+ end
484
+
485
+ # Given a string, attempt to parse out a value from that string. This
486
+ # value might be continued on the following line. So this method returns
487
+ # `true` if it is expecting more data.
488
+ #
489
+ # string - String to parse
490
+ #
491
+ # Returns `true` if the next line is also part of the current value.
492
+ # Returns `fase` if the string contained a complete value.
493
+ def parse_value( string )
494
+ continuation = false
495
+
496
+ # if our value starts with a double quote, then we are in a
497
+ # line continuation situation
498
+ if leading_quote?
499
+ # check for a closing quote at the end of the string
500
+ if string =~ @close_quote
501
+ value << $1
502
+
503
+ # otherwise just append the string to the value
504
+ else
505
+ value << string
506
+ continuation = true
507
+ end
508
+
509
+ # not currently processing a continuation line
510
+ else
511
+ case string
512
+ when @full_quote
513
+ self.value = $1
514
+
515
+ when @open_quote
516
+ self.value = $1
517
+ continuation = true
518
+
519
+ when @trailing_slash
520
+ self.value ? self.value << $1 : self.value = $1
521
+ continuation = @continuation
522
+
523
+ when @normal_value
524
+ self.value ? self.value << $1 : self.value = $1
525
+
526
+ else
527
+ error
528
+ end
529
+ end
530
+
531
+ if continuation
532
+ self.value << $/ if leading_quote?
533
+ else
534
+ process_property
535
+ end
536
+
537
+ continuation
538
+ end
539
+
540
+ # Parse the ini file contents. This will clear any values currently stored
541
+ # in the ini hash.
542
+ #
543
+ # content - Any object that responds to `each_line`
544
+ #
545
+ # Returns nil.
546
+ def parse( content )
547
+ return unless content
548
+
549
+ continuation = false
550
+
551
+ @hash.clear
552
+ @line = nil
553
+ self.section = nil
554
+
555
+ content.each_line do |line|
556
+ @line = line.chomp
557
+
558
+ if continuation
559
+ continuation = parse_value @line
560
+ else
561
+ case @line
562
+ when @ignore_regexp
563
+ nil
564
+ when @section_regexp
565
+ self.section = @hash[$1]
566
+ when @property_regexp
567
+ self.property = $1.strip
568
+ error if property.empty?
569
+
570
+ continuation = parse_value $2
571
+ when @switch_regexp
572
+ #self.property = $1.strip
573
+ self.section[$1.strip] = {}
574
+ else
575
+ error
576
+ end
577
+ end
578
+ end
579
+ # check here if we have a dangling value ... usually means we have an
580
+ # unmatched open quote
581
+ if leading_quote?
582
+ error "Unmatched open quote"
583
+ elsif property && value
584
+ process_property
585
+ elsif value
586
+ error
587
+ end
588
+
589
+ nil
590
+ end
591
+
592
+ # Store the property/value pair in the currently active section. This
593
+ # method checks for continuation of the value to the next line.
594
+ #
595
+ # Returns nil.
596
+ def process_property
597
+ property.strip!
598
+ value.strip!
599
+
600
+ self.value = $1 if value =~ %r/\A"(.*)(?<!\\)"\z/m
601
+
602
+ if section[property].nil? || !@force_array
603
+ section[property] = typecast(value)
604
+ elsif section[property].is_a?(Array)
605
+ section[property] << typecast(value)
606
+ else
607
+ section[property] = [section[property], typecast(value)]
608
+ end
609
+
610
+ self.property = nil
611
+ self.value = nil
612
+ end
613
+
614
+ # Returns the current section Hash.
615
+ def section
616
+ @section ||= @hash[@default]
617
+ end
618
+
619
+ # Raise a parse error using the given message and appending the current line
620
+ # being parsed.
621
+ #
622
+ # msg - The message String to use.
623
+ #
624
+ # Raises IniFile::Error
625
+ def error( msg = 'Could not parse line' )
626
+ raise Error, "#{msg}: #{@line.inspect}"
627
+ end
628
+
629
+ # Attempt to typecast the value string. We are looking for boolean values,
630
+ # integers, floats, and empty strings. Below is how each gets cast, but it
631
+ # is pretty logical and straightforward.
632
+ #
633
+ # "true" --> true
634
+ # "false" --> false
635
+ # "" --> nil
636
+ # "42" --> 42
637
+ # "3.14" --> 3.14
638
+ # "foo" --> "foo"
639
+ #
640
+ # Returns the typecast value.
641
+ def typecast( value )
642
+ case value
643
+ when %r/\Atrue\z/i; true
644
+ when %r/\Afalse\z/i; false
645
+ when %r/\A\s*\z/i; nil
646
+ else
647
+ stripped_value = value.strip
648
+ if stripped_value =~ /^\d*\.\d+$/
649
+ Float(stripped_value)
650
+ elsif stripped_value =~ /^[^0]\d*$/
651
+ Integer(stripped_value)
652
+ else
653
+ unescape_value(value)
654
+ end
655
+ end
656
+ rescue
657
+ unescape_value(value)
658
+ end
659
+
660
+ # Unescape special characters found in the value string. This will convert
661
+ # escaped null, tab, carriage return, newline, and backslash into their
662
+ # literal equivalents.
663
+ #
664
+ # value - The String value to unescape.
665
+ #
666
+ # Returns the unescaped value.
667
+ def unescape_value( value )
668
+ value = value.to_s
669
+ value.gsub!(%r/\\[0nrt\\]/) { |char|
670
+ case char
671
+ when '\0'; "\0"
672
+ when '\n'; "\n"
673
+ when '\r'; "\r"
674
+ when '\t'; "\t"
675
+ when '\\\\'; "\\"
676
+ end
677
+ }
678
+ value
679
+ end
680
+ end
681
+
682
+ end # IniFile