Module: Puppet::Util::FileParsing

Includes:
Puppet::Util
Included in:
Provider::ParsedFile
Defined in:
lib/puppet/util/fileparsing.rb

Overview

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.

Defined Under Namespace

Classes: FileRecord

Constant Summary

Constants included from Puppet::Util

ALNUM, ALPHA, AbsolutePathPosix, AbsolutePathWindows, DEFAULT_POSIX_MODE, DEFAULT_WINDOWS_MODE, ESCAPED, HEX, HttpProxy, PUPPET_STACK_INSERTION_FRAME, RESERVED, RFC_3986_URI_REGEX, UNRESERVED, UNSAFE

Constants included from POSIX

POSIX::LOCALE_ENV_VARS, POSIX::USER_ENV_VARS

Constants included from SymbolicFileMode

SymbolicFileMode::SetGIDBit, SymbolicFileMode::SetUIDBit, SymbolicFileMode::StickyBit, SymbolicFileMode::SymbolicMode, SymbolicFileMode::SymbolicSpecialToBit

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Puppet::Util

absolute_path?, benchmark, chuser, clear_environment, create_erb, default_env, deterministic_rand, deterministic_rand_int, exit_on_fail, format_backtrace_array, format_puppetstack_frame, get_env, get_environment, logmethods, merge_environment, path_to_uri, pretty_backtrace, replace_file, resolve_stackframe, rfc2396_escape, safe_posix_fork, set_env, skip_external_facts, symbolizehash, thinmark, uri_encode, uri_query_encode, uri_to_path, uri_unescape, which, withenv, withumask

Methods included from POSIX

#get_posix_field, #gid, groups_of, #idfield, #methodbyid, #methodbyname, #search_posix_field, #uid

Methods included from SymbolicFileMode

#display_mode, #normalize_symbolic_mode, #symbolic_mode_to_int, #valid_symbolic_mode?

Instance Attribute Details

#line_separatorObject



237
238
239
240
241
# File 'lib/puppet/util/fileparsing.rb', line 237

def line_separator
  @line_separator ||= "\n"

  @line_separator
end

#trailing_separatorObject

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



367
368
369
370
371
372
373
# File 'lib/puppet/util/fileparsing.rb', line 367

def trailing_separator
  if defined?(@trailing_separator)
    @trailing_separator
  else
    true
  end
end

Instance Method Details

#clear_recordsObject

Clear all existing record definitions. Only used for testing.



148
149
150
151
# File 'lib/puppet/util/fileparsing.rb', line 148

def clear_records
  @record_types.clear
  @record_order.clear
end

#fields(type) ⇒ Object



153
154
155
156
157
158
159
160
# File 'lib/puppet/util/fileparsing.rb', line 153

def fields(type)
  record = record_type(type)
  if record
    record.fields.dup
  else
    nil
  end
end

#handle_record_line(line, record) ⇒ Hash<Symbol, Object>

Try to match a record.

Parameters:

Returns:

  • (Hash<Symbol, Object>)

    The parsed elements of the line



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/puppet/util/fileparsing.rb', line 173

def handle_record_line(line, record)
  ret = nil
  if record.respond_to?(:process)
    ret = record.send(:process, line.dup)
    if ret
      unless ret.is_a?(Hash)
        raise Puppet::DevError, _("Process record type %{record_name} returned non-hash") % { record_name: record.name }
      end
    else
      return nil
    end
  else
    regex = record.match
    if regex
      # In this case, we try to match the whole line and then use the
      # match captures to get our fields.
      match = regex.match(line)
      if match
        ret = {}
        record.fields.zip(match.captures).each do |field, value|
          if value == record.absent
            ret[field] = :absent
          else
            ret[field] = value
          end
        end
      else
        nil
      end
    else
      ret = {}
      sep = record.separator

      # String "helpfully" replaces ' ' with /\s+/ in splitting, so we
      # have to work around it.
      if sep == " "
        sep = / /
      end
      line_fields = line.split(sep)
      record.fields.each do |param|
        value = line_fields.shift
        if value and value != record.absent
          ret[param] = value
        else
          ret[param] = :absent
        end
      end

      if record.rollup and !line_fields.empty?
        last_field = record.fields[-1]
        val = ([ret[last_field]] + line_fields).join(record.joiner)
        ret[last_field] = val
      end
    end
  end

  if ret
    ret[:record_type] = record.name
    ret
  else
    nil
  end
end

#handle_text_line(line, record) ⇒ Object

Try to match a specific text line.



163
164
165
# File 'lib/puppet/util/fileparsing.rb', line 163

def handle_text_line(line, record)
  line =~ record.match ? { :record_type => record.name, :line => line } : nil
end

#lines(text) ⇒ Object

Split text into separate lines using the record separator.



244
245
246
247
248
# File 'lib/puppet/util/fileparsing.rb', line 244

def lines(text)
  # NOTE: We do not have to remove trailing separators because split will ignore
  # them by default (unless you pass -1 as a second parameter)
  text.split(line_separator)
end

#parse(text) ⇒ Object

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



251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/puppet/util/fileparsing.rb', line 251

def parse(text)
  count = 1
  lines(text).collect do |line|
    count += 1
    val = parse_line(line)
    if val
      val
    else
      error = Puppet::ResourceError.new(_("Could not parse line %{line}") % { line: line.inspect })
      error.line = count
      raise error
    end
  end
end

#parse_line(line) ⇒ Object

Handle parsing a single line.

Raises:



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/puppet/util/fileparsing.rb', line 267

def parse_line(line)
  raise Puppet::DevError, _("No record types defined; cannot parse lines") unless records?

  @record_order.each do |record|
    # These are basically either text or record lines.
    method = "handle_#{record.type}_line"
    if respond_to?(method)
      result = send(method, line, record)
      if result
        record.send(:post_parse, result) if record.respond_to?(:post_parse)
        return result
      end
    else
      raise Puppet::DevError, _("Somehow got invalid line type %{record_type}") % { record_type: record.type }
    end
  end

  nil
end

#record_line(name, options, &block) ⇒ Object

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+/.

Raises:

  • (ArgumentError)


301
302
303
304
305
306
307
308
# File 'lib/puppet/util/fileparsing.rb', line 301

def record_line(name, options, &block)
  raise ArgumentError, _("Must include a list of fields") unless options.include?(:fields)

  record = FileRecord.new(:record, **options, &block)
  record.name = name.intern

  new_line_type(record)
end

#records?Boolean

Are there any record types defined?

Returns:

  • (Boolean)


311
312
313
# File 'lib/puppet/util/fileparsing.rb', line 311

def records?
  defined?(@record_types) and !@record_types.empty?
end

#text_line(name, options, &block) ⇒ Object

Define a new type of text record.

Raises:

  • (ArgumentError)


316
317
318
319
320
321
322
323
# File 'lib/puppet/util/fileparsing.rb', line 316

def text_line(name, options, &block)
  raise ArgumentError, _("You must provide a :match regex for text lines") unless options.include?(:match)

  record = FileRecord.new(:text, **options, &block)
  record.name = name.intern

  new_line_type(record)
end

#to_file(records) ⇒ Object

Generate a file from a bunch of hash records.



326
327
328
329
330
331
332
# File 'lib/puppet/util/fileparsing.rb', line 326

def to_file(records)
  text = records.collect { |record| to_line(record) }.join(line_separator)

  text += line_separator if trailing_separator

  text
end

#to_line(details) ⇒ Object

Convert our parsed record into a text record.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/puppet/util/fileparsing.rb', line 335

def to_line(details)
  record = record_type(details[:record_type])
  unless record
    raise ArgumentError, _("Invalid record type %{record_type}") % { record_type: details[:record_type].inspect }
  end

  if record.respond_to?(:pre_gen)
    details = details.dup
    record.send(:pre_gen, details)
  end

  case record.type
  when :text; details[:line]
  else
    return record.to_line(details) if record.respond_to?(:to_line)

    line = record.join(details)

    regex = record.rts
    if regex
      # If they say true, then use whitespace; else, use their regex.
      if regex == true
        regex = /\s+$/
      end
      line.sub(regex, '')
    else
      line
    end
  end
end

#valid_attr?(type, attr) ⇒ Boolean

Returns:

  • (Boolean)


375
376
377
378
379
380
381
382
383
# File 'lib/puppet/util/fileparsing.rb', line 375

def valid_attr?(type, attr)
  type = type.intern
  record = record_type(type)
  if record && record.fields.include?(attr.intern)
    true
  else
    attr.intern == :ensure
  end
end