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.