Mercurial > repos > other > Puppet
diff modules/inifile/lib/puppet/util/ini_file.rb @ 386:3fce34f642f1
Add a PHP module to handle platform differences
author | IBBoard <dev@ibboard.co.uk> |
---|---|
date | Mon, 03 Jan 2022 17:09:39 +0000 |
parents | |
children | adf6fe9bbc17 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/inifile/lib/puppet/util/ini_file.rb Mon Jan 03 17:09:39 2022 +0000 @@ -0,0 +1,349 @@ +# frozen_string_literal: true + +require File.expand_path('../external_iterator', __FILE__) +require File.expand_path('../ini_file/section', __FILE__) + +module Puppet::Util + # + # ini_file.rb + # + class IniFile + def initialize(path, key_val_separator = ' = ', section_prefix = '[', section_suffix = ']', + indent_char = ' ', indent_width = nil) + + k_v_s = (key_val_separator =~ %r{^\s+$}) ? ' ' : key_val_separator.strip + + @section_prefix = section_prefix + @section_suffix = section_suffix + @indent_char = indent_char + @indent_width = indent_width ? indent_width.to_i : nil + + @section_regex = section_regex + @setting_regex = %r{^(\s*)([^#;\s]|[^#;\s].*?[^\s#{k_v_s}])(\s*#{k_v_s}[ \t]*)(.*)\s*$} + @commented_setting_regex = %r{^(\s*)[#;]+(\s*)(.*?[^\s#{k_v_s}])(\s*#{k_v_s}[ \t]*)(.*)\s*$} + + @path = path + @key_val_separator = key_val_separator + @section_names = [] + @sections_hash = {} + parse_file + end + + def section_regex + # Only put in prefix/suffix if they exist + # Also, if the prefix is '', the negated + # set match should be a match all instead. + r_string = '^\s*' + r_string += Regexp.escape(@section_prefix) + r_string += '(' + if @section_prefix != '' + r_string += '[^' + r_string += Regexp.escape(@section_prefix) + r_string += ']' + else + r_string += '.' + end + r_string += '*)' + r_string += Regexp.escape(@section_suffix) + r_string += '\s*$' + %r{#{r_string}} + end + + attr_reader :section_names + + def get_settings(section_name) + section = @sections_hash[section_name] + section.setting_names.each_with_object({}) do |setting, result| + result[setting] = section.get_value(setting) + end + end + + def section?(section_name) + @sections_hash.key?(section_name) + end + + def get_value(section_name, setting) + @sections_hash[section_name].get_value(setting) if @sections_hash.key?(section_name) + end + + def set_value(*args) + case args.size + when 1 + section_name = args[0] + when 3 + # Backwards compatible set_value function, See MODULES-5172 + (section_name, setting, value) = args + when 4 + (section_name, setting, separator, value) = args + end + + complete_setting = { + setting: setting, + separator: separator, + value: value, + } + unless @sections_hash.key?(section_name) + add_section(Section.new(section_name, nil, nil, nil, nil)) + end + + section = @sections_hash[section_name] + + if section.existing_setting?(setting) + update_line(section, setting, value) + section.update_existing_setting(setting, value) + elsif 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(find_commented_setting(section, setting), section, complete_setting) + + # 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) + elsif !setting.nil? || !value.nil? + section.set_additional_setting(setting, value) + end + end + + def remove_setting(section_name, setting) + section = @sections_hash[section_name] + return unless section.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) + + return unless section.empty? + # By convention, it's time to remove this newly emptied out section + lines.delete_at(section.start_line) + decrement_section_line_numbers(section_index + 1) + @section_names.delete_at(section_index) + @sections_hash.delete(section.name) + end + + def save + global_empty = @sections_hash[''].empty? && @sections_hash[''].additional_settings.empty? + File.open(@path, 'w') do |fh| + @section_names.each_index do |index| + name = @section_names[index] + + section = @sections_hash[name] + + # We need a buffer to cache lines that are only whitespace + whitespace_buffer = [] + + if section.new_section? && !section.global? + if index == 1 && !global_empty || index > 1 + fh.puts('') + end + + fh.puts("#{@section_prefix}#{section.name}#{@section_suffix}") + end + + unless section.new_section? + # write all of the pre-existing lines + (section.start_line..section.end_line).each do |line_num| + line = lines[line_num] + + # We buffer any lines that are only whitespace so that + # if they are at the end of a section, we can insert + # any new settings *before* the final chunk of whitespace + # lines. + if line.match?(%r{^\s*$}) + whitespace_buffer << line + else + # If we get here, we've found a non-whitespace line. + # We'll flush any cached whitespace lines before we + # write it. + flush_buffer_to_file(whitespace_buffer, fh) + fh.puts(line) + end + end + end + + # write new settings, if there are any + section.additional_settings.each_pair do |key, value| + fh.puts("#{@indent_char * (@indent_width || section.indentation || 0)}#{key}#{@key_val_separator}#{value}") + end + + if !whitespace_buffer.empty? + flush_buffer_to_file(whitespace_buffer, fh) + elsif section.new_section? && !section.additional_settings.empty? && (index < @section_names.length - 1) + # We get here if there were no blank lines at the end of the + # section. + # + # If we are adding a new section with a new setting, + # and if there are more sections that come after this one, + # we'll write one blank line just so that there is a little + # whitespace between the sections. + # if (section.end_line.nil? && + fh.puts('') + 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 = start_line + min_indentation = nil + empty = true + loop do + line, line_num = line_iter.peek + if line_num.nil? || @section_regex.match(line) + # the global section always exists, even when it's empty; + # when it's empty, we must be sure it's thought of as new, + # which is signalled with a nil ending line + end_line_num = nil if name == '' && empty + return Section.new(name, start_line, end_line_num, settings, min_indentation) + end + if (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 + empty = false + line_iter.next + end + end + + def update_line(section, setting, value) + (section.start_line..section.end_line).each do |line_num| + next unless (match = @setting_regex.match(lines[line_num])) + if match[2] == setting + lines[line_num] = "#{match[1]}#{match[2]}#{match[3]}#{value}" + end + end + end + + def remove_line(section, setting) + (section.start_line..section.end_line).each do |line_num| + next unless (match = @setting_regex.match(lines[line_num])) + if match[2] == setting + lines.delete_at(line_num) + 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) # rubocop:disable Lint/IneffectiveAccessModifier : Attempting to change breaks tests + # 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.file?(path) ? 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.new_section? + (section.start_line..section.end_line).each do |line_num| + next unless (match = @commented_setting_regex.match(lines[line_num])) + if match[3] == setting + return { match: match, line_num: line_num } + 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, complete_setting) + line_num = result[:line_num] + s = complete_setting + lines.insert(line_num + 1, "#{@indent_char * (@indent_width || section.indentation || 0)}#{s[:setting]}#{s[:separator]}#{s[: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 + # 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 + + # 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) + return if buffer.empty? + buffer.each { |l| fh.puts(l) } + buffer.clear + end + end +end