Treetop PEG for Puppet resources

Earlier this year at Puppet Camp EU, Randall Hansen ran an open space session on improving the Puppet user experience.

Lots of sharp edges were identified, but one issue that I raised was the annoying need for trailing commas to break up parameters in resource declarations.

I chatted about this briefly with Luke and for a laugh I decided to write a Treetop Parsing Expression Grammar (PEG) for Puppet resources that supported newlines as the parameter delimeter:

# puppet.treetop
grammar Puppet
  rule resource
    whitespace
    type
    whitespace
    open
    whitespace
    name
    whitespace
    parameters
    whitespace
    close
    whitespace
    {
      def resource_type
        type.text_value
      end

      def resource_name
        name.word.text_value
      end
    }
  end

  rule type
    word
  end

  rule open
    "{"
  end

  rule close
    "}"
  end

  rule name
    quotes word quotes ":"
    {
      def name
        word
      end
    }
  end

  rule word
    [a-zA-Z]+
  end

  rule quotes
   "'" / '"'
  end

  rule parameters
    newline* (whitespace parameter comma_or_newline*)*
  end

  rule parameter
    whitespace
    word
    whitespace
    arrow
    whitespace
    word
    whitespace
  end

  rule arrow
    "=>"
  end

  rule comma_or_newline
    comma / newline
  end

  rule comma
    ","
  end

  rule newline
    "\n"
  end

  rule whitespace
    "\s"* / "\n"+
  end
end

It’s throwaway code, but as far as I’m aware it’s relatively idiomatic Treetop.

It came in handy earlier this week when explaining PEGs to a new recruit into the R&D team at work.

Said recruit suggested that I publish it, as there aren’t too many examples of Treetop PEGs floating around.

To run the PEG over an example snippet:

#!/usr/bin/env ruby

require 'rubygems'
require 'bundler/setup'
require 'polyglot'
require 'treetop'

Treetop.load "puppet"

snippet = <<-SNIPPET
  package { "foobar":
    ensure => present, another => bar, spoons => doom
    foo    => bar
  }
SNIPPET

parser = PuppetParser.new
if @root = parser.parse(snippet.strip)
  puts 'success'
  p @root.resource_type
  p @root.resource_name
else
  puts 'failure'
  puts parser.failure_reason
  puts parser.failure_column
  puts parser.failure_line
  puts parser.failure_index
end

Gemfile for running it and all the above code is in a Gist.