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
Public Instance Methods
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
# 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
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
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
# File lib/puppet/util/fileparsing.rb 237 def line_separator 238 @line_separator ||= "\n" 239 240 @line_separator 241 end
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
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
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
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
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
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
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
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
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
# 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
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
Retrieve the record object.
# File lib/puppet/util/fileparsing.rb 405 def record_type(type) 406 @record_types[type.intern] 407 end