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

Try to match a specific text line.

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

Split text into separate lines using the record separator.

    # File lib/puppet/util/fileparsing.rb
243 def lines(text)
244   # NOTE: We do not have to remove trailing separators because split will ignore
245   # them by default (unless you pass -1 as a second parameter)
246   text.split(self.line_separator)
247 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
250 def parse(text)
251   count = 1
252   lines(text).collect do |line|
253     count += 1
254     val = parse_line(line)
255     if val
256       val
257     else
258       error = Puppet::ResourceError.new(_("Could not parse line %{line}") % { line: line.inspect })
259       error.line = count
260       raise error
261     end
262   end
263 end
parse_line(line) click to toggle source

Handle parsing a single line.

    # File lib/puppet/util/fileparsing.rb
266 def parse_line(line)
267   raise Puppet::DevError, _("No record types defined; cannot parse lines") unless records?
268 
269   @record_order.each do |record|
270     # These are basically either text or record lines.
271     method = "handle_#{record.type}_line"
272     if respond_to?(method)
273       result = send(method, line, record)
274       if result
275         record.send(:post_parse, result) if record.respond_to?(:post_parse)
276         return result
277       end
278     else
279       raise Puppet::DevError, _("Somehow got invalid line type %{record_type}") % { record_type: record.type }
280     end
281   end
282 
283   nil
284 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
300 def record_line(name, options, &block)
301   raise ArgumentError, _("Must include a list of fields") unless options.include?(:fields)
302 
303   record = FileRecord.new(:record, **options, &block)
304   record.name = name.intern
305 
306   new_line_type(record)
307 end
records?() click to toggle source

Are there any record types defined?

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

Define a new type of text record.

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

Generate a file from a bunch of hash records.

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

Convert our parsed record into a text record.

    # File lib/puppet/util/fileparsing.rb
334 def to_line(details)
335   record = record_type(details[:record_type])
336   unless record
337     raise ArgumentError, _("Invalid record type %{record_type}") % { record_type: details[:record_type].inspect }
338   end
339 
340   if record.respond_to?(:pre_gen)
341     details = details.dup
342     record.send(:pre_gen, details)
343   end
344 
345   case record.type
346   when :text; return details[:line]
347   else
348     return record.to_line(details) if record.respond_to?(:to_line)
349 
350     line = record.join(details)
351 
352     regex = record.rts
353     if regex
354       # If they say true, then use whitespace; else, use their regex.
355       if regex == true
356         regex = /\s+$/
357       end
358       return line.sub(regex,'')
359     else
360       return line
361     end
362   end
363 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
366 def trailing_separator
367   if defined?(@trailing_separator)
368     return @trailing_separator
369   else
370     return true
371   end
372 end
valid_attr?(type, attr) click to toggle source
    # File lib/puppet/util/fileparsing.rb
374 def valid_attr?(type, attr)
375   type = type.intern
376   record = record_type(type)
377   if record && record.fields.include?(attr.intern)
378     return true
379   else
380     if attr.intern == :ensure
381       return true
382     else
383       false
384     end
385   end
386 end

Private Instance Methods

new_line_type(record) click to toggle source

Define a new type of record.

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

Retrieve the record object.

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