diff --git a/lib/puppet/util/inifile.rb b/lib/puppet/util/inifile.rb index df3c59850..17e76d1f7 100644 --- a/lib/puppet/util/inifile.rb +++ b/lib/puppet/util/inifile.rb @@ -1,218 +1,222 @@ # Module Puppet::IniConfig # A generic way to parse .ini style files and manipulate them in memory # One 'file' can be made up of several physical files. Changes to sections # on the file are tracked so that only the physical files in which # something has changed are written back to disk # Great care is taken to preserve comments and blank lines from the original # files # # The parsing tries to stay close to python's ConfigParser require 'puppet/util/filetype' module Puppet::Util::IniConfig # A section in a .ini file class Section attr_reader :name, :file, :entries attr_writer :destroy def initialize(name, file) @name = name @file = file @dirty = false @entries = [] @destroy = false end # Has this section been modified since it's been read in # or written back to disk def dirty? @dirty end + def mark_dirty + @dirty = true + end + # Should only be used internally def mark_clean @dirty = false end # Should the file be destroyed? def destroy? @destroy end # Add a line of text (e.g., a comment) Such lines # will be written back out in exactly the same # place they were read in def add_line(line) @entries << line end # Set the entry 'key=value'. If no entry with the # given key exists, one is appended to teh end of the section def []=(key, value) entry = find_entry(key) @dirty = true if entry.nil? @entries << [key, value] else entry[1] = value end end # Return the value associated with KEY. If no such entry # exists, return nil def [](key) entry = find_entry(key) return(entry.nil? ? nil : entry[1]) end # Format the section as text in the way it should be # written to file def format text = "[#{name}]\n" @entries.each do |entry| if entry.is_a?(Array) key, value = entry text << "#{key}=#{value}\n" unless value.nil? else text << entry end end text end private def find_entry(key) @entries.each do |entry| return entry if entry.is_a?(Array) && entry[0] == key end nil end end # A logical .ini-file that can be spread across several physical # files. For each physical file, call #read with the filename class File def initialize @files = {} end # Add the contents of the file with name FILE to the # already existing sections def read(file) text = Puppet::Util::FileType.filetype(:flat).new(file).read raise "Could not find #{file}" if text.nil? section = nil # The name of the current section optname = nil # The name of the last option in section line = 0 @files[file] = [] text.each_line do |l| line += 1 if l.strip.empty? || "#;".include?(l[0,1]) || (l.split(nil, 2)[0].downcase == "rem" && l[0,1].downcase == "r") # Whitespace or comment if section.nil? @files[file] << l else section.add_line(l) end elsif " \t\r\n\f".include?(l[0,1]) && section && optname # continuation line section[optname] += "\n#{l.chomp}" elsif l =~ /^\[([^\]]+)\]/ # section heading section.mark_clean unless section.nil? section = add_section($1, file) optname = nil elsif l =~ /^\s*([^\s=]+)\s*\=(.*)$/ # We allow space around the keys, but not the values # For the values, we don't know if space is significant if section.nil? raise "#{file}:#{line}:Key/value pair outside of a section for key #{$1}" else section[$1] = $2 optname = $1 end else raise "#{file}:#{line}: Can't parse '#{l.chomp}'" end end section.mark_clean unless section.nil? end # Store all modifications made to sections in this file back # to the physical files. If no modifications were made to # a physical file, nothing is written def store @files.each do |file, lines| text = "" dirty = false destroy = false lines.each do |l| if l.is_a?(Section) destroy ||= l.destroy? dirty ||= l.dirty? text << l.format l.mark_clean else text << l end end # We delete the file and then remove it from the list of files. if destroy ::File.unlink(file) @files.delete(file) else if dirty Puppet::Util::FileType.filetype(:flat).new(file).write(text) return file end end end end # Execute BLOCK, passing each section in this file # as an argument def each_section(&block) @files.each do |file, list| list.each do |entry| yield(entry) if entry.is_a?(Section) end end end # Execute BLOCK, passing each file constituting this inifile # as an argument def each_file(&block) @files.keys.each do |file| yield(file) end end # Return the Section with the given name or nil def [](name) name = name.to_s each_section do |section| return section if section.name == name end nil end # Return true if the file contains a section with name NAME def include?(name) ! self[name].nil? end # Add a section to be stored in FILE when store is called def add_section(name, file) raise "A section with name #{name} already exists" if include?(name) result = Section.new(name, file) @files[file] ||= [] @files[file] << result result end end end diff --git a/spec/unit/util/inifile_spec.rb b/spec/unit/util/inifile_spec.rb new file mode 100644 index 000000000..3be3378e7 --- /dev/null +++ b/spec/unit/util/inifile_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' +require 'puppet/util/inifile' + +describe Puppet::Util::IniConfig::Section do + + subject { described_class.new('testsection', '/some/imaginary/file') } + + describe "determining if the section is dirty" do + it "is not dirty on creation" do + expect(subject).to_not be_dirty + end + + it "is dirty if a key is changed" do + subject['hello'] = 'world' + expect(subject).to be_dirty + end + + it "is dirty if the section has been explicitly marked as dirty" do + subject.mark_dirty + expect(subject).to be_dirty + end + + it "is clean if the section has been explicitly marked as clean" do + subject['hello'] = 'world' + subject.mark_clean + expect(subject).to_not be_dirty + end + end + + describe "reading an entry" do + it "returns nil if the key is not present" do + expect(subject['hello']).to be_nil + end + + it "returns the value if the key is specified" do + subject.entries << ['hello', 'world'] + expect(subject['hello']).to eq 'world' + end + + it "ignores comments when looking for a match" do + subject.entries << '#this = comment' + expect(subject['#this']).to be_nil + end + end + + describe "formatting the section" do + it "prefixes the output with the section header" do + expect(subject.format).to eq "[testsection]\n" + end + + it "restores comments and blank lines" do + subject.entries << "#comment\n" + subject.entries << " " + expect(subject.format).to eq( + "[testsection]\n" + + "#comment\n" + + " " + ) + end + + it "adds all keys that have values" do + subject.entries << ['somekey', 'somevalue'] + expect(subject.format).to eq("[testsection]\nsomekey=somevalue\n") + end + + it "excludes keys that have a value of nil" do + subject.entries << ['empty', nil] + expect(subject.format).to eq("[testsection]\n") + end + + it "preserves the order of the section" do + subject.entries << ['firstkey', 'firstval'] + subject.entries << "# I am a comment, hear me roar\n" + subject.entries << ['secondkey', 'secondval'] + + expect(subject.format).to eq( + "[testsection]\n" + + "firstkey=firstval\n" + + "# I am a comment, hear me roar\n" + + "secondkey=secondval\n" + ) + end + end +end