]> git.donarmstrong.com Git - dsa-puppet.git/blob - 3rdparty/modules/inifile/lib/puppet/util/ini_file.rb
add puppetlabs/inifile to 3rdparty
[dsa-puppet.git] / 3rdparty / modules / inifile / lib / puppet / util / ini_file.rb
1 require File.expand_path('../external_iterator', __FILE__)
2 require File.expand_path('../ini_file/section', __FILE__)
3
4 module Puppet
5 module Util
6   class IniFile
7
8     def initialize(path, key_val_separator = ' = ', section_prefix = '[', section_suffix = ']')
9
10       k_v_s = key_val_separator.strip
11
12       @section_prefix = section_prefix
13       @section_suffix = section_suffix
14
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*$/
18
19       @path = path
20       @key_val_separator = key_val_separator
21       @section_names = []
22       @sections_hash = {}
23       if File.file?(@path)
24         parse_file
25       end
26     end
27
28     def section_regex
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.
32       r_string = '^\s*'
33       r_string += Regexp.escape(@section_prefix)
34       r_string += '('
35       if @section_prefix != ''
36         r_string += '[^'
37         r_string += Regexp.escape(@section_prefix)
38         r_string += ']'
39       else
40         r_string += '.'
41       end
42       r_string += '*)'
43       r_string += Regexp.escape(@section_suffix)
44       r_string += '\s*$'
45       /#{r_string}/
46     end
47
48     def section_names
49       @section_names
50     end
51
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)
56         result
57       end
58     end
59
60     def get_value(section_name, setting)
61       if (@sections_hash.has_key?(section_name))
62         @sections_hash[section_name].get_value(setting)
63       end
64     end
65
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))
69       end
70
71       section = @sections_hash[section_name]
72
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.
83
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)
87
88         # Then, we need to tell the setting object that we hacked
89         # in an inline setting
90         section.insert_inline_setting(setting, value)
91
92         # Finally, we need to update all of the start/end line
93         # numbers for all of the sections *after* the one that
94         # was modified.
95         section_index = @section_names.index(section_name)
96         increment_section_line_numbers(section_index + 1)
97       else
98         section.set_additional_setting(setting, value)
99       end
100     end
101
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)
108
109         # Then, we need to tell the setting object to remove
110         # the setting from its state:
111         section.remove_existing_setting(setting)
112
113         # Finally, we need to update all of the start/end line
114         # numbers for all of the sections *after* the one that
115         # was modified.
116         section_index = @section_names.index(section_name)
117         decrement_section_line_numbers(section_index + 1)
118       end
119     end
120
121     def save
122       File.open(@path, 'w') do |fh|
123
124         @section_names.each_index do |index|
125           name = @section_names[index]
126
127           section = @sections_hash[name]
128
129           # We need a buffer to cache lines that are only whitespace
130           whitespace_buffer = []
131
132           if (section.is_new_section?) && (! section.is_global?)
133             fh.puts("\n#{@section_prefix}#{section.name}#{@section_suffix}")
134           end
135
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]
140
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
144               # lines.
145               if (line =~ /^\s*$/)
146                 whitespace_buffer << line
147               else
148                 # If we get here, we've found a non-whitespace line.
149                 # We'll flush any cached whitespace lines before we
150                 # write it.
151                 flush_buffer_to_file(whitespace_buffer, fh)
152                 fh.puts(line)
153               end
154             end
155           end
156
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}")
160           end
161
162           if (whitespace_buffer.length > 0)
163             flush_buffer_to_file(whitespace_buffer, fh)
164           else
165             # We get here if there were no blank lines at the end of the
166             # section.
167             #
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))
176               fh.puts("")
177             end
178           end
179
180         end
181       end
182     end
183
184
185     private
186     def add_section(section)
187       @sections_hash[section.name] = section
188       @section_names << section.name
189     end
190
191     def parse_file
192       line_iter = create_line_iter
193
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)
197       add_section(section)
198       line, line_num = line_iter.next
199
200       while line
201         if (match = @@SECTION_REGEX.match(line))
202           section = read_section(match[1], line_num, line_iter)
203           add_section(section)
204         end
205         line, line_num = line_iter.next
206       end
207     end
208
209     def read_section(name, start_line, line_iter)
210       settings = {}
211       end_line_num = nil
212       min_indentation = nil
213       while true
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
221         end
222         end_line_num = line_num
223         line_iter.next
224       end
225     end
226
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}"
232           end
233         end
234       end
235     end
236
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)
242           end
243         end
244       end
245     end
246
247     def create_line_iter
248       ExternalIterator.new(lines)
249     end
250
251     def lines
252         @lines ||= IniFile.readlines(@path)
253     end
254
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
262         #  too much trouble.
263         File.readlines(path)
264     end
265
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
269     # two keys:
270     #
271     #   :line_num - the line number that contains the commented version
272     #               of the setting
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 }
281           end
282         end
283       end
284       nil
285     end
286
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}")
294     end
295
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
303       end
304     end
305
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
313       end
314     end
315
316
317     def flush_buffer_to_file(buffer, fh)
318       if buffer.length > 0
319         buffer.each { |l| fh.puts(l) }
320         buffer.clear
321       end
322     end
323
324   end
325 end
326 end