c2c26de9b1
This commit adds some cosmetic functionality. The main two improvements are: * We'll now pay attention to indentation within existing sections, and when we write new settings or update existing ones, we'll match the existing indentation. * When modifying existing settings, the regex now captures a greater portion of the original line and preserves it. Basically, the original whitespacing in the line should remain intact and only the value should be modified.
181 lines
5.4 KiB
Ruby
181 lines
5.4 KiB
Ruby
require File.expand_path('../external_iterator', __FILE__)
|
|
require File.expand_path('../ini_file/section', __FILE__)
|
|
|
|
module Puppet
|
|
module Util
|
|
class IniFile
|
|
|
|
SECTION_REGEX = /^\s*\[([\w\d\.\\\/\-\:]+)\]\s*$/
|
|
SETTING_REGEX = /^(\s*)([\w\d\.\\\/\-]+)(\s*=\s*)([\S\s]*\S)\s*$/
|
|
|
|
def initialize(path, key_val_separator = ' = ')
|
|
@path = path
|
|
@key_val_separator = key_val_separator
|
|
@section_names = []
|
|
@sections_hash = {}
|
|
if File.file?(@path)
|
|
parse_file
|
|
end
|
|
end
|
|
|
|
def section_names
|
|
@section_names
|
|
end
|
|
|
|
def get_value(section_name, setting)
|
|
if (@sections_hash.has_key?(section_name))
|
|
@sections_hash[section_name].get_value(setting)
|
|
end
|
|
end
|
|
|
|
def set_value(section_name, setting, value)
|
|
unless (@sections_hash.has_key?(section_name))
|
|
add_section(Section.new(section_name, nil, nil, nil, nil))
|
|
end
|
|
|
|
section = @sections_hash[section_name]
|
|
if (section.has_existing_setting?(setting))
|
|
update_line(section, setting, value)
|
|
section.update_existing_setting(setting, value)
|
|
else
|
|
section.set_additional_setting(setting, value)
|
|
end
|
|
end
|
|
|
|
def remove_setting(section_name, setting)
|
|
section = @sections_hash[section_name]
|
|
if (section.has_existing_setting?(setting))
|
|
# If the setting is found, we have some work to do.
|
|
# First, we remove the line from our array of lines:
|
|
remove_line(section, setting)
|
|
|
|
# Then, we need to tell the setting object to remove
|
|
# the setting from its state:
|
|
section.remove_existing_setting(setting)
|
|
|
|
# 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)
|
|
decrement_section_line_numbers(section_index + 1)
|
|
end
|
|
end
|
|
|
|
def save
|
|
File.open(@path, 'w') do |fh|
|
|
|
|
@section_names.each do |name|
|
|
|
|
section = @sections_hash[name]
|
|
|
|
if section.start_line.nil?
|
|
fh.puts("\n[#{section.name}]")
|
|
elsif ! section.end_line.nil?
|
|
(section.start_line..section.end_line).each do |line_num|
|
|
fh.puts(lines[line_num])
|
|
end
|
|
end
|
|
|
|
section.additional_settings.each_pair do |key, value|
|
|
fh.puts("#{' ' * (section.indentation || 0)}#{key}#{@key_val_separator}#{value}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
private
|
|
def add_section(section)
|
|
@sections_hash[section.name] = section
|
|
@section_names << section.name
|
|
end
|
|
|
|
def parse_file
|
|
line_iter = create_line_iter
|
|
|
|
# We always create a "global" section at the beginning of the file, for
|
|
# anything that appears before the first named section.
|
|
section = read_section('', 0, line_iter)
|
|
add_section(section)
|
|
line, line_num = line_iter.next
|
|
|
|
while line
|
|
if (match = SECTION_REGEX.match(line))
|
|
section = read_section(match[1], line_num, line_iter)
|
|
add_section(section)
|
|
end
|
|
line, line_num = line_iter.next
|
|
end
|
|
end
|
|
|
|
def read_section(name, start_line, line_iter)
|
|
settings = {}
|
|
end_line_num = nil
|
|
min_indentation = nil
|
|
while true
|
|
line, line_num = line_iter.peek
|
|
if (line_num.nil? or match = SECTION_REGEX.match(line))
|
|
return Section.new(name, start_line, end_line_num, settings, min_indentation)
|
|
elsif (match = SETTING_REGEX.match(line))
|
|
settings[match[2]] = match[4]
|
|
indentation = match[1].length
|
|
min_indentation = [indentation, min_indentation || indentation].min
|
|
end
|
|
end_line_num = line_num
|
|
line_iter.next
|
|
end
|
|
end
|
|
|
|
def update_line(section, setting, value)
|
|
(section.start_line..section.end_line).each do |line_num|
|
|
if (match = SETTING_REGEX.match(lines[line_num]))
|
|
if (match[2] == setting)
|
|
lines[line_num] = "#{match[1]}#{match[2]}#{match[3]}#{value}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def remove_line(section, setting)
|
|
(section.start_line..section.end_line).each do |line_num|
|
|
if (match = SETTING_REGEX.match(lines[line_num]))
|
|
if (match[2] == setting)
|
|
lines.delete_at(line_num)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_line_iter
|
|
ExternalIterator.new(lines)
|
|
end
|
|
|
|
def lines
|
|
@lines ||= IniFile.readlines(@path)
|
|
end
|
|
|
|
# This is mostly here because it makes testing easier--we don't have
|
|
# to try to stub any methods on File.
|
|
def self.readlines(path)
|
|
# If this type is ever used with very large files, we should
|
|
# write this in a different way, using a temp
|
|
# file; for now assuming that this type is only used on
|
|
# small-ish config files that can fit into memory without
|
|
# too much trouble.
|
|
File.readlines(path)
|
|
end
|
|
|
|
|
|
# Utility method; given a section index (index into the @section_names
|
|
# array), decrement the start/end line numbers for that section and all
|
|
# all of the other sections that appear *after* the specified section.
|
|
def decrement_section_line_numbers(section_index)
|
|
@section_names[section_index..(@section_names.length - 1)].each do |name|
|
|
section = @sections_hash[name]
|
|
section.decrement_line_nums
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|