module Puppet::Util::FileParsing

A mini-language for parsing files. This is only used file the ParsedFile provider, but it makes more sense to split it out so it's easy to maintain in one place.

You can use this module to create simple parser/generator classes. For instance, the following parser should go most of the way to parsing /etc/passwd:

class Parser
    include Puppet::Util::FileParsing
    record_line :user, :fields => %w{name password uid gid gecos home shell},
        :separator => ":"
end

You would use it like this:

parser = Parser.new
lines = parser.parse(File.read("/etc/passwd"))

lines.each do |type, hash| # type will always be :user, since we only have one
    p hash
end

Each line in this case would be a hash, with each field set appropriately. You could then call 'parser.to_line(hash)' on any of those hashes to generate the text line again.

Attributes

line_separator[W]
trailing_separator[W]

Public Instance Methods

clear_records() click to toggle source

Clear all existing record definitions. Only used for testing.

    # File lib/puppet/util/fileparsing.rb
148 def clear_records
149   @record_types.clear
150   @record_order.clear
151 end
fields(type) click to toggle source
    # File lib/puppet/util/fileparsing.rb
153 def fields(type)
154   record = record_type(type)
155   if record
156     record.fields.dup
157   else
158     nil
159   end
160 end
handle_record_line(line, record) click to toggle source

Try to match a record.

@param [String] line The line to be parsed @param [Puppet::Util::FileType] record The filetype to use for parsing

@return [Hash<Symbol, Object>] The parsed elements of the line

    # File lib/puppet/util/fileparsing.rb
173 def handle_record_line(line, record)
174   ret = nil
175   if record.respond_to?(:process)
176     ret = record.send(:process, line.dup)
177     if ret
178       unless ret.is_a?(Hash)
179         raise Puppet::DevError, _("Process record type %{record_name} returned non-hash") % { record_name: record.name }
180       end
181     else
182       return nil
183     end
184   else
185     regex = record.match
186     if regex
187       # In this case, we try to match the whole line and then use the
188       # match captures to get our fields.
189       match = regex.match(line)
190       if match
191         ret = {}
192         record.fields.zip(match.captures).each do |field, value|
193           if value == record.absent
194             ret[field] = :absent
195           else
196             ret[field] = value
197           end
198         end
199       else
200         nil
201       end
202     else
203       ret = {}
204       sep = record.separator
205 
206       # String "helpfully" replaces ' ' with /\s+/ in splitting, so we
207       # have to work around it.
208       if sep == " "
209         sep = / /
210       end
211       line_fields = line.split(sep)
212       record.fields.each do |param|
213         value = line_fields.shift
214         if value and value != record.absent
215           ret[param] = value
216         else
217           ret[param] = :absent
218         end
219       end
220 
221       if record.rollup and ! line_fields.empty?
222         last_field = record.fields[-1]
223         val = ([ret[last_field]] + line_fields).join(record.joiner)
224         ret[last_field] = val
225       end
226     end
227   end
228 
229   if ret
230     ret[:record_type] = record.name
231     return ret
232   else
233     return nil
234   end
235 end
handle_text_line(line, record) click to toggle source

Try to match a specific text line.

    # File lib/puppet/util/fileparsing.rb
163 def handle_text_line(line, record)
164   line =~ record.match ? {:record_type => record.name, :line => line} : nil
165 end
line_separator() click to toggle source
    # File lib/puppet/util/fileparsing.rb
237 def line_separator
238   @line_separator ||= "\n"
239 
240   @line_separator
241 end
lines(text) click to toggle source

Split text into separate lines using the record separator.

    # File lib/puppet/util/fileparsing.rb
244 def lines(text)
245   # NOTE: We do not have to remove trailing separators because split will ignore
246   # them by default (unless you pass -1 as a second parameter)
247   text.split(self.line_separator)
248 end
parse(text) click to toggle source

Split a bunch of text into lines and then parse them individually.

    # File lib/puppet/util/fileparsing.rb
251 def parse(text)
252   count = 1
253   lines(text).collect do |line|
254     count += 1
255     val = parse_line(line)
256     if val
257       val
258     else
259       error = Puppet::ResourceError.new(_("Could not parse line %{line}") % { line: line.inspect })
260       error.line = count
261       raise error
262     end
263   end
264 end
parse_line(line) click to toggle source

Handle parsing a single line.

    # File lib/puppet/util/fileparsing.rb
267 def parse_line(line)
268   raise Puppet::DevError, _("No record types defined; cannot parse lines") unless records?
269 
270   @record_order.each do |record|
271     # These are basically either text or record lines.
272     method = "handle_#{record.type}_line"
273     if respond_to?(method)
274       result = send(method, line, record)
275       if result
276         record.send(:post_parse, result) if record.respond_to?(:post_parse)
277         return result
278       end
279     else
280       raise Puppet::DevError, _("Somehow got invalid line type %{record_type}") % { record_type: record.type }
281     end
282   end
283 
284   nil
285 end
record_line(name, options, &block) click to toggle source

Define a new type of record. These lines get split into hashes. Valid options are:

  • :absent: What to use as value within a line, when a field is absent. Note that in the record object, the literal :absent symbol is used, and not this value. Defaults to “”.

  • :fields: The list of fields, as an array. By default, all fields are considered required.

  • :joiner: How to join fields together. Defaults to 't'.

  • :optional: Which fields are optional. If these are missing, you'll just get the 'absent' value instead of an ArgumentError.

  • :rts: Whether to remove trailing whitespace. Defaults to false. If true, whitespace will be removed; if a regex, then whatever matches the regex will be removed.

  • :separator: The record separator. Defaults to /s+/.

    # File lib/puppet/util/fileparsing.rb
301 def record_line(name, options, &block)
302   raise ArgumentError, _("Must include a list of fields") unless options.include?(:fields)
303 
304   record = FileRecord.new(:record, **options, &block)
305   record.name = name.intern
306 
307   new_line_type(record)
308 end
records?() click to toggle source

Are there any record types defined?

    # File lib/puppet/util/fileparsing.rb
311 def records?
312   defined?(@record_types) and ! @record_types.empty?
313 end
text_line(name, options, &block) click to toggle source

Define a new type of text record.

    # File lib/puppet/util/fileparsing.rb
316 def text_line(name, options, &block)
317   raise ArgumentError, _("You must provide a :match regex for text lines") unless options.include?(:match)
318 
319   record = FileRecord.new(:text, **options, &block)
320   record.name = name.intern
321 
322   new_line_type(record)
323 end
to_file(records) click to toggle source

Generate a file from a bunch of hash records.

    # File lib/puppet/util/fileparsing.rb
326 def to_file(records)
327   text = records.collect { |record| to_line(record) }.join(line_separator)
328 
329   text += line_separator if trailing_separator
330 
331   text
332 end
to_line(details) click to toggle source

Convert our parsed record into a text record.

    # File lib/puppet/util/fileparsing.rb
335 def to_line(details)
336   record = record_type(details[:record_type])
337   unless record
338     raise ArgumentError, _("Invalid record type %{record_type}") % { record_type: details[:record_type].inspect }
339   end
340 
341   if record.respond_to?(:pre_gen)
342     details = details.dup
343     record.send(:pre_gen, details)
344   end
345 
346   case record.type
347   when :text; return details[:line]
348   else
349     return record.to_line(details) if record.respond_to?(:to_line)
350 
351     line = record.join(details)
352 
353     regex = record.rts
354     if regex
355       # If they say true, then use whitespace; else, use their regex.
356       if regex == true
357         regex = /\s+$/
358       end
359       return line.sub(regex,'')
360     else
361       return line
362     end
363   end
364 end
trailing_separator() click to toggle source

Whether to add a trailing separator to the file. Defaults to true

    # File lib/puppet/util/fileparsing.rb
367 def trailing_separator
368   if defined?(@trailing_separator)
369     return @trailing_separator
370   else
371     return true
372   end
373 end
valid_attr?(type, attr) click to toggle source
    # File lib/puppet/util/fileparsing.rb
375 def valid_attr?(type, attr)
376   type = type.intern
377   record = record_type(type)
378   if record && record.fields.include?(attr.intern)
379     return true
380   else
381     if attr.intern == :ensure
382       return true
383     else
384       false
385     end
386   end
387 end

Private Instance Methods

new_line_type(record) click to toggle source

Define a new type of record.

    # File lib/puppet/util/fileparsing.rb
392 def new_line_type(record)
393   @record_types ||= {}
394   @record_order ||= []
395 
396   raise ArgumentError, _("Line type %{name} is already defined") % { name: record.name } if @record_types.include?(record.name)
397 
398   @record_types[record.name] = record
399   @record_order << record
400 
401   record
402 end
record_type(type) click to toggle source

Retrieve the record object.

    # File lib/puppet/util/fileparsing.rb
405 def record_type(type)
406   @record_types[type.intern]
407 end