4

I have a groovy config file that I want to append data too. It would be easier to gather data using python that I want to add but I couldn't find a corresponding ConfigSlurper module in python and there is no straightforward way I saw to be able to do this using ConfigParser or anything. Has anyone done anything like this that has some feedback/advice on best approach?

Ben John
  • 53
  • 1
  • 4

2 Answers2

8

That was a fun exercise.

from shlex import shlex
from ast import literal_eval

TRANSLATION = {
        "true": True,
        "false": False,
        "null": None,
    }

class ParseException(Exception):
    def __init__(self, token, line):
        self.token = token
        self.line = line
    def __str__(self):
        return "ParseException at line %d: invalid token %s" % (self.line, self.token)

class GroovyConfigSlurper:
    def __init__(self, source):
        self.source = source

    def parse(self):
        lex = shlex(self.source)
        lex.wordchars += "."
        state = 1
        context = []
        result = dict()
        while 1:
            token = lex.get_token()
            if not token:
                return result
            if state == 1:
                if token == "}":
                    if len(context):
                        context.pop()
                    else:
                        raise ParseException(token, lex.lineno)
                else:
                    name = token
                    state = 2
            elif state == 2:
                if token == "=":
                    state = 3
                elif token == "{":
                    context.append(name)
                    state = 1
                else:
                    raise ParseException(token, lex.lineno)
            elif state == 3:
                try:
                    value = TRANSLATION[token]
                except KeyError:
                    value = literal_eval(token)
                key = ".".join(context + [name]).split(".")
                current = result
                for i in xrange(0, len(key) - 1):
                    if key[i] not in current:
                        current[key[i]] = dict()
                    current = current[key[i]]
                current[key[-1]] = value
                state = 1

Then, you can do

with open("test.conf", "r") as f:
    print GroovyConfigSlurper(f).parse()
# => {'setting': {'smtp': {'mail': {'host': 'smtp.myisp.com', 'auth': {'user': 'server'}}}}, 'grails': {'webflow': {'stateless': True}}, 'resources': {'URL': 'http://localhost:80/resources'}}
Amadan
  • 191,408
  • 23
  • 240
  • 301
  • 3
    Looks promising, but it bails when I try to parse a build.gradle file, which is a Groovy DSL: "ParseException at line 1: invalid token plugin" line 1 is: "apply plugin: 'com.android.application'" – vitriolix Jan 29 '16 at 17:54
  • @vitriolix did you ever get a solution to this? – ThunderGrad Oct 23 '20 at 17:42
0

@Amadan's answer above works great. I might also suggest two small modifications, they were needed in our case (parsing Groovy based Nextflow Config files from within a Python code-base):

  1. support negative numbers (i.e. a monadic minus sign)
  2. support Groovy-style single-line comments (i.e //)

Also added a simple utility method to write the JSON to file and added the ability to pass in a string file-name instead of a string object.

The updated code looks like this:

from shlex import shlex
from ast import literal_eval

TRANSLATION = {
    "true": True,
    "false": False,
    "null": None,
}


class ParseException(Exception):
    def __init__(self, token, line):
        self.token = token
        self.line = line

    def __str__(self):
        return "ParseException at line %d: invalid token %s" % (self.line, self.token)


class GroovyConfigParser:
    def __init__(self, source):
        if isinstance(source, str):
            self.source = open(source)
            self.should_close_source = True
        else:
            self.source = source
            self.should_close_source = False

    def __del__(self):
        if self.should_close_source and not self.source.closed:
            self.source.close()

    def parse(self):
        lex = shlex(self.source)
        lex.wordchars = lex.wordchars + ".-"
        lex.commenters = "//"
        state = 1
        context = []
        result = dict()
        while True:
            token = lex.get_token()
            if not token:
                return result
            if state == 1:
                if token == "}":
                    if len(context):
                        context.pop()
                    else:
                        raise ParseException(token, lex.lineno)
                else:
                    name = token
                    state = 2
            elif state == 2:
                if token == "=":
                    state = 3
                elif token == "{":
                    context.append(name)
                    state = 1
                else:
                    raise ParseException(token, lex.lineno)
            elif state == 3:
                try:
                    value = TRANSLATION[token]
                except KeyError:
                    value = literal_eval(token)
                key = ".".join(context + [name]).split(".")
                current = result
                for i in range(len(key) - 1):
                    if key[i] not in current:
                        current[key[i]] = dict()
                    current = current[key[i]]
                current[key[-1]] = value
                state = 1

    def write_as_json_file(self, json_file):
        import json
        with open(json_file, 'w') as file:
            json.dump(self.parse(), file, indent=4)