1 require File.expand_path('../external_iterator', __FILE__)
2 require File.expand_path('../ini_file/section', __FILE__)
8 def initialize(path, key_val_separator = ' = ', section_prefix = '[', section_suffix = ']')
10 k_v_s = key_val_separator.strip
12 @section_prefix = section_prefix
13 @section_suffix = section_suffix
15 @@SECTION_REGEX = section_regex
16 @@SETTING_REGEX = /^(\s*)([^#;\s]|[^#;\s].*?[^\s#{k_v_s}])(\s*#{k_v_s}\s*)(.*)\s*$/
17 @@COMMENTED_SETTING_REGEX = /^(\s*)[#;]+(\s*)(.*?[^\s#{k_v_s}])(\s*#{k_v_s}[ \t]*)(.*)\s*$/
20 @key_val_separator = key_val_separator
29 # Only put in prefix/suffix if they exist
30 # Also, if the prefix is '', the negated
31 # set match should be a match all instead.
33 r_string += Regexp.escape(@section_prefix)
35 if @section_prefix != ''
37 r_string += Regexp.escape(@section_prefix)
43 r_string += Regexp.escape(@section_suffix)
52 def get_settings(section_name)
53 section = @sections_hash[section_name]
54 section.setting_names.inject({}) do |result, setting|
55 result[setting] = section.get_value(setting)
60 def get_value(section_name, setting)
61 if (@sections_hash.has_key?(section_name))
62 @sections_hash[section_name].get_value(setting)
66 def set_value(section_name, setting, value)
67 unless (@sections_hash.has_key?(section_name))
68 add_section(Section.new(section_name, nil, nil, nil, nil))
71 section = @sections_hash[section_name]
73 if (section.has_existing_setting?(setting))
74 update_line(section, setting, value)
75 section.update_existing_setting(setting, value)
76 elsif result = find_commented_setting(section, setting)
77 # So, this stanza is a bit of a hack. What we're trying
78 # to do here is this: for settings that don't already
79 # exist, we want to take a quick peek to see if there
80 # is a commented-out version of them in the section.
81 # If so, we'd prefer to add the setting directly after
82 # the commented line, rather than at the end of the section.
84 # If we get here then we found a commented line, so we
85 # call "insert_inline_setting_line" to update the lines array
86 insert_inline_setting_line(result, section, setting, value)
88 # Then, we need to tell the setting object that we hacked
89 # in an inline setting
90 section.insert_inline_setting(setting, value)
92 # Finally, we need to update all of the start/end line
93 # numbers for all of the sections *after* the one that
95 section_index = @section_names.index(section_name)
96 increment_section_line_numbers(section_index + 1)
98 section.set_additional_setting(setting, value)
102 def remove_setting(section_name, setting)
103 section = @sections_hash[section_name]
104 if (section.has_existing_setting?(setting))
105 # If the setting is found, we have some work to do.
106 # First, we remove the line from our array of lines:
107 remove_line(section, setting)
109 # Then, we need to tell the setting object to remove
110 # the setting from its state:
111 section.remove_existing_setting(setting)
113 # Finally, we need to update all of the start/end line
114 # numbers for all of the sections *after* the one that
116 section_index = @section_names.index(section_name)
117 decrement_section_line_numbers(section_index + 1)
122 File.open(@path, 'w') do |fh|
124 @section_names.each_index do |index|
125 name = @section_names[index]
127 section = @sections_hash[name]
129 # We need a buffer to cache lines that are only whitespace
130 whitespace_buffer = []
132 if (section.is_new_section?) && (! section.is_global?)
133 fh.puts("\n#{@section_prefix}#{section.name}#{@section_suffix}")
136 if ! section.is_new_section?
137 # write all of the pre-existing settings
138 (section.start_line..section.end_line).each do |line_num|
139 line = lines[line_num]
141 # We buffer any lines that are only whitespace so that
142 # if they are at the end of a section, we can insert
143 # any new settings *before* the final chunk of whitespace
146 whitespace_buffer << line
148 # If we get here, we've found a non-whitespace line.
149 # We'll flush any cached whitespace lines before we
151 flush_buffer_to_file(whitespace_buffer, fh)
157 # write new settings, if there are any
158 section.additional_settings.each_pair do |key, value|
159 fh.puts("#{' ' * (section.indentation || 0)}#{key}#{@key_val_separator}#{value}")
162 if (whitespace_buffer.length > 0)
163 flush_buffer_to_file(whitespace_buffer, fh)
165 # We get here if there were no blank lines at the end of the
168 # If we are adding a new section with a new setting,
169 # and if there are more sections that come after this one,
170 # we'll write one blank line just so that there is a little
171 # whitespace between the sections.
172 #if (section.end_line.nil? &&
173 if (section.is_new_section? &&
174 (section.additional_settings.length > 0) &&
175 (index < @section_names.length - 1))
186 def add_section(section)
187 @sections_hash[section.name] = section
188 @section_names << section.name
192 line_iter = create_line_iter
194 # We always create a "global" section at the beginning of the file, for
195 # anything that appears before the first named section.
196 section = read_section('', 0, line_iter)
198 line, line_num = line_iter.next
201 if (match = @@SECTION_REGEX.match(line))
202 section = read_section(match[1], line_num, line_iter)
205 line, line_num = line_iter.next
209 def read_section(name, start_line, line_iter)
212 min_indentation = nil
214 line, line_num = line_iter.peek
215 if (line_num.nil? or match = @@SECTION_REGEX.match(line))
216 return Section.new(name, start_line, end_line_num, settings, min_indentation)
217 elsif (match = @@SETTING_REGEX.match(line))
218 settings[match[2]] = match[4]
219 indentation = match[1].length
220 min_indentation = [indentation, min_indentation || indentation].min
222 end_line_num = line_num
227 def update_line(section, setting, value)
228 (section.start_line..section.end_line).each do |line_num|
229 if (match = @@SETTING_REGEX.match(lines[line_num]))
230 if (match[2] == setting)
231 lines[line_num] = "#{match[1]}#{match[2]}#{match[3]}#{value}"
237 def remove_line(section, setting)
238 (section.start_line..section.end_line).each do |line_num|
239 if (match = @@SETTING_REGEX.match(lines[line_num]))
240 if (match[2] == setting)
241 lines.delete_at(line_num)
248 ExternalIterator.new(lines)
252 @lines ||= IniFile.readlines(@path)
255 # This is mostly here because it makes testing easier--we don't have
256 # to try to stub any methods on File.
257 def self.readlines(path)
258 # If this type is ever used with very large files, we should
259 # write this in a different way, using a temp
260 # file; for now assuming that this type is only used on
261 # small-ish config files that can fit into memory without
266 # This utility method scans through the lines for a section looking for
267 # commented-out versions of a setting. It returns `nil` if it doesn't
268 # find one. If it does find one, then it returns a hash containing
271 # :line_num - the line number that contains the commented version
273 # :match - the ruby regular expression match object, which can
274 # be used to mimic the whitespace from the comment line
275 def find_commented_setting(section, setting)
276 return nil if section.is_new_section?
277 (section.start_line..section.end_line).each do |line_num|
278 if (match = @@COMMENTED_SETTING_REGEX.match(lines[line_num]))
279 if (match[3] == setting)
280 return { :match => match, :line_num => line_num }
287 # This utility method is for inserting a line into the existing
288 # lines array. The `result` argument is expected to be in the
289 # format of the return value of `find_commented_setting`.
290 def insert_inline_setting_line(result, section, setting, value)
291 line_num = result[:line_num]
292 match = result[:match]
293 lines.insert(line_num + 1, "#{' ' * (section.indentation || 0 )}#{setting}#{match[4]}#{value}")
296 # Utility method; given a section index (index into the @section_names
297 # array), decrement the start/end line numbers for that section and all
298 # all of the other sections that appear *after* the specified section.
299 def decrement_section_line_numbers(section_index)
300 @section_names[section_index..(@section_names.length - 1)].each do |name|
301 section = @sections_hash[name]
302 section.decrement_line_nums
306 # Utility method; given a section index (index into the @section_names
307 # array), increment the start/end line numbers for that section and all
308 # all of the other sections that appear *after* the specified section.
309 def increment_section_line_numbers(section_index)
310 @section_names[section_index..(@section_names.length - 1)].each do |name|
311 section = @sections_hash[name]
312 section.increment_line_nums
317 def flush_buffer_to_file(buffer, fh)
319 buffer.each { |l| fh.puts(l) }