ini_file.rb 11 KB

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