ini_file.rb 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. require File.expand_path('../external_iterator', __FILE__)
  2. require File.expand_path('../ini_file/section', __FILE__)
  3. module Puppet
  4. module Util
  5. class IniFile
  6. def initialize(path, key_val_separator = ' = ')
  7. k_v_s = key_val_separator.strip
  8. @@SECTION_REGEX = /^\s*\[([^\]]*)\]\s*$/
  9. @@SETTING_REGEX = /^(\s*)([^\s#{k_v_s}]*)(\s*#{k_v_s}\s*)(.*)\s*$/
  10. @@COMMENTED_SETTING_REGEX = /^(\s*)[#;]+(\s*)([^\s#{k_v_s}]*)(\s*#{k_v_s}[ \t]*)(.*)\s*$/
  11. @path = path
  12. @key_val_separator = key_val_separator
  13. @section_names = []
  14. @sections_hash = {}
  15. if File.file?(@path)
  16. parse_file
  17. end
  18. end
  19. def section_names
  20. @section_names
  21. end
  22. def get_settings(section_name)
  23. section = @sections_hash[section_name]
  24. section.setting_names.inject({}) do |result, setting|
  25. result[setting] = section.get_value(setting)
  26. result
  27. end
  28. end
  29. def get_value(section_name, setting)
  30. if (@sections_hash.has_key?(section_name))
  31. @sections_hash[section_name].get_value(setting)
  32. end
  33. end
  34. def set_value(section_name, setting, value)
  35. unless (@sections_hash.has_key?(section_name))
  36. add_section(Section.new(section_name, nil, nil, nil, nil))
  37. end
  38. section = @sections_hash[section_name]
  39. if (section.has_existing_setting?(setting))
  40. update_line(section, setting, value)
  41. section.update_existing_setting(setting, value)
  42. elsif result = find_commented_setting(section, setting)
  43. # So, this stanza is a bit of a hack. What we're trying
  44. # to do here is this: for settings that don't already
  45. # exist, we want to take a quick peek to see if there
  46. # is a commented-out version of them in the section.
  47. # If so, we'd prefer to add the setting directly after
  48. # the commented line, rather than at the end of the section.
  49. # If we get here then we found a commented line, so we
  50. # call "insert_inline_setting_line" to update the lines array
  51. insert_inline_setting_line(result, section, setting, value)
  52. # Then, we need to tell the setting object that we hacked
  53. # in an inline setting
  54. section.insert_inline_setting(setting, value)
  55. # Finally, we need to update all of the start/end line
  56. # numbers for all of the sections *after* the one that
  57. # was modified.
  58. section_index = @section_names.index(section_name)
  59. increment_section_line_numbers(section_index + 1)
  60. else
  61. section.set_additional_setting(setting, value)
  62. end
  63. end
  64. def remove_setting(section_name, setting)
  65. section = @sections_hash[section_name]
  66. if (section.has_existing_setting?(setting))
  67. # If the setting is found, we have some work to do.
  68. # First, we remove the line from our array of lines:
  69. remove_line(section, setting)
  70. # Then, we need to tell the setting object to remove
  71. # the setting from its state:
  72. section.remove_existing_setting(setting)
  73. # Finally, we need to update all of the start/end line
  74. # numbers for all of the sections *after* the one that
  75. # was modified.
  76. section_index = @section_names.index(section_name)
  77. decrement_section_line_numbers(section_index + 1)
  78. end
  79. end
  80. def save
  81. File.open(@path, 'w') do |fh|
  82. @section_names.each_index do |index|
  83. name = @section_names[index]
  84. section = @sections_hash[name]
  85. # We need a buffer to cache lines that are only whitespace
  86. whitespace_buffer = []
  87. if (section.is_new_section?) && (! section.is_global?)
  88. fh.puts("\n[#{section.name}]")
  89. end
  90. if ! section.is_new_section?
  91. # write all of the pre-existing settings
  92. (section.start_line..section.end_line).each do |line_num|
  93. line = lines[line_num]
  94. # We buffer any lines that are only whitespace so that
  95. # if they are at the end of a section, we can insert
  96. # any new settings *before* the final chunk of whitespace
  97. # lines.
  98. if (line =~ /^\s*$/)
  99. whitespace_buffer << line
  100. else
  101. # If we get here, we've found a non-whitespace line.
  102. # We'll flush any cached whitespace lines before we
  103. # write it.
  104. flush_buffer_to_file(whitespace_buffer, fh)
  105. fh.puts(line)
  106. end
  107. end
  108. end
  109. # write new settings, if there are any
  110. section.additional_settings.each_pair do |key, value|
  111. fh.puts("#{' ' * (section.indentation || 0)}#{key}#{@key_val_separator}#{value}")
  112. end
  113. if (whitespace_buffer.length > 0)
  114. flush_buffer_to_file(whitespace_buffer, fh)
  115. else
  116. # We get here if there were no blank lines at the end of the
  117. # section.
  118. #
  119. # If we are adding a new section with a new setting,
  120. # and if there are more sections that come after this one,
  121. # we'll write one blank line just so that there is a little
  122. # whitespace between the sections.
  123. #if (section.end_line.nil? &&
  124. if (section.is_new_section? &&
  125. (section.additional_settings.length > 0) &&
  126. (index < @section_names.length - 1))
  127. fh.puts("")
  128. end
  129. end
  130. end
  131. end
  132. end
  133. private
  134. def add_section(section)
  135. @sections_hash[section.name] = section
  136. @section_names << section.name
  137. end
  138. def parse_file
  139. line_iter = create_line_iter
  140. # We always create a "global" section at the beginning of the file, for
  141. # anything that appears before the first named section.
  142. section = read_section('', 0, line_iter)
  143. add_section(section)
  144. line, line_num = line_iter.next
  145. while line
  146. if (match = @@SECTION_REGEX.match(line))
  147. section = read_section(match[1], line_num, line_iter)
  148. add_section(section)
  149. end
  150. line, line_num = line_iter.next
  151. end
  152. end
  153. def read_section(name, start_line, line_iter)
  154. settings = {}
  155. end_line_num = nil
  156. min_indentation = nil
  157. while true
  158. line, line_num = line_iter.peek
  159. if (line_num.nil? or match = @@SECTION_REGEX.match(line))
  160. return Section.new(name, start_line, end_line_num, settings, min_indentation)
  161. elsif (match = @@SETTING_REGEX.match(line))
  162. settings[match[2]] = match[4]
  163. indentation = match[1].length
  164. min_indentation = [indentation, min_indentation || indentation].min
  165. end
  166. end_line_num = line_num
  167. line_iter.next
  168. end
  169. end
  170. def update_line(section, setting, value)
  171. (section.start_line..section.end_line).each do |line_num|
  172. if (match = @@SETTING_REGEX.match(lines[line_num]))
  173. if (match[2] == setting)
  174. lines[line_num] = "#{match[1]}#{match[2]}#{match[3]}#{value}"
  175. end
  176. end
  177. end
  178. end
  179. def remove_line(section, setting)
  180. (section.start_line..section.end_line).each do |line_num|
  181. if (match = @@SETTING_REGEX.match(lines[line_num]))
  182. if (match[2] == setting)
  183. lines.delete_at(line_num)
  184. end
  185. end
  186. end
  187. end
  188. def create_line_iter
  189. ExternalIterator.new(lines)
  190. end
  191. def lines
  192. @lines ||= IniFile.readlines(@path)
  193. end
  194. # This is mostly here because it makes testing easier--we don't have
  195. # to try to stub any methods on File.
  196. def self.readlines(path)
  197. # If this type is ever used with very large files, we should
  198. # write this in a different way, using a temp
  199. # file; for now assuming that this type is only used on
  200. # small-ish config files that can fit into memory without
  201. # too much trouble.
  202. File.readlines(path)
  203. end
  204. # This utility method scans through the lines for a section looking for
  205. # commented-out versions of a setting. It returns `nil` if it doesn't
  206. # find one. If it does find one, then it returns a hash containing
  207. # two keys:
  208. #
  209. # :line_num - the line number that contains the commented version
  210. # of the setting
  211. # :match - the ruby regular expression match object, which can
  212. # be used to mimic the whitespace from the comment line
  213. def find_commented_setting(section, setting)
  214. return nil if section.is_new_section?
  215. (section.start_line..section.end_line).each do |line_num|
  216. if (match = @@COMMENTED_SETTING_REGEX.match(lines[line_num]))
  217. if (match[3] == setting)
  218. return { :match => match, :line_num => line_num }
  219. end
  220. end
  221. end
  222. nil
  223. end
  224. # This utility method is for inserting a line into the existing
  225. # lines array. The `result` argument is expected to be in the
  226. # format of the return value of `find_commented_setting`.
  227. def insert_inline_setting_line(result, section, setting, value)
  228. line_num = result[:line_num]
  229. match = result[:match]
  230. lines.insert(line_num + 1, "#{' ' * (section.indentation || 0 )}#{setting}#{match[4]}#{value}")
  231. end
  232. # Utility method; given a section index (index into the @section_names
  233. # array), decrement the start/end line numbers for that section and all
  234. # all of the other sections that appear *after* the specified section.
  235. def decrement_section_line_numbers(section_index)
  236. @section_names[section_index..(@section_names.length - 1)].each do |name|
  237. section = @sections_hash[name]
  238. section.decrement_line_nums
  239. end
  240. end
  241. # Utility method; given a section index (index into the @section_names
  242. # array), increment the start/end line numbers for that section and all
  243. # all of the other sections that appear *after* the specified section.
  244. def increment_section_line_numbers(section_index)
  245. @section_names[section_index..(@section_names.length - 1)].each do |name|
  246. section = @sections_hash[name]
  247. section.increment_line_nums
  248. end
  249. end
  250. def flush_buffer_to_file(buffer, fh)
  251. if buffer.length > 0
  252. buffer.each { |l| fh.puts(l) }
  253. buffer.clear
  254. end
  255. end
  256. end
  257. end
  258. end