o-inifile 4.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 +18 -0
- data/.travis.yml +12 -0
- data/History.txt +59 -0
- data/README.md +243 -0
- data/Rakefile +24 -0
- data/inifile.gemspec +24 -0
- data/lib/inifile.rb +682 -0
- data/lib/o-inifile.rb +1 -0
- data/test/data/bad_1.ini +6 -0
- data/test/data/bad_2.ini +11 -0
- data/test/data/browscap.ini +5 -0
- data/test/data/comment.ini +10 -0
- data/test/data/continuation.ini +6 -0
- data/test/data/escape.ini +13 -0
- data/test/data/force_array.ini +5 -0
- data/test/data/global.ini +3 -0
- data/test/data/good.ini +19 -0
- data/test/data/line_continuation.ini +13 -0
- data/test/data/merge.ini +5 -0
- data/test/data/mixed_comment.ini +7 -0
- data/test/data/multiline.ini +24 -0
- data/test/data/param.ini +5 -0
- data/test/data/section.ini +4 -0
- data/test/data/switch.ini +3 -0
- data/test/data/typecast.ini +7 -0
- data/test/test_inifile.rb +629 -0
- metadata +117 -0
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
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
|