Browse Source

Add detection for commented versions of settings

This commit adds support for detecting commented versions of
settings in an existing version of an inifile.  If you are
setting a value for a setting that isn't currently set
in the file, but a commented version is found, then we
add the new setting immediately following the commented
version, rather than at the end of the section.
Chris Price 11 years ago
parent
commit
a45ab65

+ 63 - 0
lib/puppet/util/ini_file.rb

@@ -7,6 +7,7 @@ module Util
 
     SECTION_REGEX = /^\s*\[([\w\d\.\\\/\-\:]+)\]\s*$/
     SETTING_REGEX = /^(\s*)([\w\d\.\\\/\-]+)(\s*=\s*)([\S\s]*\S)\s*$/
+    COMMENTED_SETTING_REGEX = /^(\s*)[#;]+(\s*)([\w\d\.\\\/\-]+)(\s*=\s*)([\S\s]*\S)\s*$/
 
     def initialize(path, key_val_separator = ' = ')
       @path = path
@@ -34,9 +35,31 @@ module Util
       end
 
       section = @sections_hash[section_name]
+
       if (section.has_existing_setting?(setting))
         update_line(section, setting, value)
         section.update_existing_setting(setting, value)
+      elsif result = find_commented_setting(section, setting)
+        # So, this stanza is a bit of a hack.  What we're trying
+        # to do here is this: for settings that don't already
+        # exist, we want to take a quick peek to see if there
+        # is a commented-out version of them in the section.
+        # If so, we'd prefer to add the setting directly after
+        # the commented line, rather than at the end of the section.
+
+        # If we get here then we found a commented line, so we
+        # call "insert_inline_setting_line" to update the lines array
+        insert_inline_setting_line(result, section, setting, value)
+
+        # Then, we need to tell the setting object that we hacked
+        # in an inline setting
+        section.insert_inline_setting(setting, value)
+
+        # Finally, we need to update all of the start/end line
+        # numbers for all of the sections *after* the one that
+        # was modified.
+        section_index = @section_names.index(section_name)
+        increment_section_line_numbers(section_index + 1)
       else
         section.set_additional_setting(setting, value)
       end
@@ -206,6 +229,35 @@ module Util
         File.readlines(path)
     end
 
+    # This utility method scans through the lines for a section looking for
+    # commented-out versions of a setting.  It returns `nil` if it doesn't
+    # find one.  If it does find one, then it returns a hash containing
+    # two keys:
+    #
+    #   :line_num - the line number that contains the commented version
+    #               of the setting
+    #   :match    - the ruby regular expression match object, which can
+    #               be used to mimic the whitespace from the comment line
+    def find_commented_setting(section, setting)
+      return nil if section.is_new_section?
+      (section.start_line..section.end_line).each do |line_num|
+        if (match = COMMENTED_SETTING_REGEX.match(lines[line_num]))
+          if (match[3] == setting)
+            return { :match => match, :line_num => line_num }
+          end
+        end
+      end
+      nil
+    end
+
+    # This utility method is for inserting a line into the existing
+    # lines array.  The `result` argument is expected to be in the
+    # format of the return value of `find_commented_setting`.
+    def insert_inline_setting_line(result, section, setting, value)
+      line_num = result[:line_num]
+      match = result[:match]
+      lines.insert(line_num + 1, "#{' ' * section.indentation}#{setting}#{match[4]}#{value}")
+    end
 
     # Utility method; given a section index (index into the @section_names
     # array), decrement the start/end line numbers for that section and all
@@ -217,6 +269,17 @@ module Util
       end
     end
 
+    # Utility method; given a section index (index into the @section_names
+    # array), increment the start/end line numbers for that section and all
+    # all of the other sections that appear *after* the specified section.
+    def increment_section_line_numbers(section_index)
+      @section_names[section_index..(@section_names.length - 1)].each do |name|
+        section = @sections_hash[name]
+        section.increment_line_nums
+      end
+    end
+
+
     def flush_buffer_to_file(buffer, fh)
       if buffer.length > 0
         buffer.each { |l| fh.puts(l) }

+ 25 - 0
lib/puppet/util/ini_file/section.rb

@@ -52,6 +52,19 @@ class IniFile
       end
     end
 
+    # This is a hacky method; it's basically called when we need to insert
+    # a new setting but we don't want it to appear at the very end of the
+    # section.  Instead we hack it into the existing settings list and
+    # increment our end_line number--this assumes that the caller (`ini_file`)
+    # is doing some babysitting w/rt the other sections and the actual data
+    # of the lines.
+    def insert_inline_setting(setting_name, value)
+      @existing_settings[setting_name] = value
+      if @end_line
+        @end_line = @end_line + 1
+      end
+    end
+
     def set_additional_setting(setting_name, value)
       @additional_settings[setting_name] = value
     end
@@ -68,6 +81,18 @@ class IniFile
       end
     end
 
+    # Increment the start and end line numbers for the section (if they are
+    # defined); this is intended to be called when an inline setting is added
+    # to a section that comes before this section in the ini file.
+    def increment_line_nums()
+      if @start_line
+        @start_line = @start_line + 1
+      end
+      if @end_line
+        @end_line = @end_line + 1
+      end
+    end
+
   end
 end
 end

+ 80 - 0
spec/unit/puppet/provider/ini_setting/ruby_spec.rb

@@ -731,4 +731,84 @@ subby=bar
 
   end
 
+
+  context "when dealing settings that have a commented version present" do
+    let(:orig_content) {
+      <<-EOS
+     [section1]
+     # foo=foovalue
+     bar=barvalue
+     foo = foovalue2
+
+[section2]
+# foo = foovalue
+;bar=barvalue
+blah = blah
+      EOS
+    }
+
+    it "should add a new setting below a commented version of that setting" do
+      resource = Puppet::Type::Ini_setting.new(
+          common_params.merge(:section => 'section2', :setting => 'foo', :value => 'foo3'))
+      provider = described_class.new(resource)
+      provider.exists?.should be_false
+      provider.create
+      validate_file(<<-EOS
+     [section1]
+     # foo=foovalue
+     bar=barvalue
+     foo = foovalue2
+
+[section2]
+# foo = foovalue
+foo = foo3
+;bar=barvalue
+blah = blah
+      EOS
+      )
+    end
+
+    it "should update an existing setting in place, even if there is a commented version of that setting" do
+      resource = Puppet::Type::Ini_setting.new(
+          common_params.merge(:section => 'section1', :setting => 'foo', :value => 'foo3'))
+      provider = described_class.new(resource)
+      provider.exists?.should be_true
+      provider.create
+      validate_file(<<-EOS
+     [section1]
+     # foo=foovalue
+     bar=barvalue
+     foo = foo3
+
+[section2]
+# foo = foovalue
+;bar=barvalue
+blah = blah
+      EOS
+      )
+    end
+
+    it "should add a new setting below a commented version of that setting, respecting semicolons as comments" do
+      resource = Puppet::Type::Ini_setting.new(
+          common_params.merge(:section => 'section2', :setting => 'bar', :value => 'bar3'))
+      provider = described_class.new(resource)
+      provider.exists?.should be_false
+      provider.create
+      validate_file(<<-EOS
+     [section1]
+     # foo=foovalue
+     bar=barvalue
+     foo = foovalue2
+
+[section2]
+# foo = foovalue
+;bar=barvalue
+bar=bar3
+blah = blah
+      EOS
+      )
+    end
+
+  end
+
 end