HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Parsing Valve Key-Value files

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
valuevalvefilesparsingkey

Problem

Introduction

Valve recently launched the Dota 2 Workshop Tools, which allows players to create their own maps with custom gamemodes, similar to Warcraft 3 in capabilities.

Ability and unit definitions are stored in txt files under a specific format, very similar to JSON:

{
    "key" "value"
    "otherKey" { //C-style comments
        "moreKey" "moreValue"
    } //no block comments!
}


It's essentialy JSON with strings only, no punctuation and comments.

I've been taking my time into writing a serializer/deserializer for such files in Lua, so that fellow map creators can use it to save arbitrary data into files. I feel like it's a bit clunky, and not the most readable solution, seeing that I'm new with Lua, this is why I've joined this site. I'd like to see which points can be improved on the code, ither with the use of convenience functions, or plainly logic changes. Even style changes are welcome, since I'm completely unaware of Lua's coding standards.
The code

Everything is contained in a 150-line Lua file, available at GitHub. Below is the mentioned file. I've wrapped everything into the KV == nil condition so I can have safely local variables and functions, essentialy creating a closure.

```
if KV == nil then
local function contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end

local chars = {
indent = "\t",
escape = "\\",
delim = '"',
commentStart = "/",
commentEnd = {"\n", "\r"},
blockStart = "{",
blockEnd = "}"
}

-- iterate_chars doc
-- receives string to operate and a iterator func
-- iterator is called for each character in the string (unless skip)
-- iterator is called with current, previous and next chars, and also
-- the remaining uniterated string
-- iterator can return false to abort iteration, or return a number

Solution

Essentially, the data format is more similar to the lua-table syntax than JSON. It'll be far easier to parse the text string (or file, which can be loaded as a string variable) to be changed to a table instead. Here's the little code I wrote (and tried).

local function comment(w)
    local r, m = w, w:match '(.+)//'
    local t, _ = m:gsub( '"', '' )
    if _ % 2 == 1 then return r else return m end
end
local function parser(x)
    local r = x:gsub( "(.-)[\n\r]+",
        function(w)
          if w:match '//' then w = comment(w) end
          w = w:gsub( '("[^"]+") {', '[%1] = {' )
          w = w:gsub( '("[^"]+") ("[^"]+")', '[%1] = %2,' )
          return w..'\n'
        end
    )
    return loadstring( "return "..r:gsub('}', '},'):gsub(',

Now, to execute (or test), you pass your string to the function parser, which'll return the key-value pairs as a table. For my testing purposes, I used the data string as follows:

{
"key" "value"
"otherKey" { //C-style "comments" "to break?"
"moreKey" "moreValue"
{"let's see" "if // breaks" } //should it?}
} //no block comments!
"let's see" "if // breaks"
}


You can use any pretty printers for printing this returned lua-table back. Do comment if you don't understand any of the steps in my program., '') )() end


Now, to execute (or test), you pass your string to the function parser, which'll return the key-value pairs as a table. For my testing purposes, I used the data string as follows:

{
"key" "value"
"otherKey" { //C-style "comments" "to break?"
"moreKey" "moreValue"
{"let's see" "if // breaks" } //should it?}
} //no block comments!
"let's see" "if // breaks"
}


You can use any pretty printers for printing this returned lua-table back. Do comment if you don't understand any of the steps in my program.

Code Snippets

local function comment(w)
    local r, m = w, w:match '(.+)//'
    local t, _ = m:gsub( '"', '' )
    if _ % 2 == 1 then return r else return m end
end
local function parser(x)
    local r = x:gsub( "(.-)[\n\r]+",
        function(w)
          if w:match '//' then w = comment(w) end
          w = w:gsub( '("[^"]+") {', '[%1] = {' )
          w = w:gsub( '("[^"]+") ("[^"]+")', '[%1] = %2,' )
          return w..'\n'
        end
    )
    return loadstring( "return "..r:gsub('}', '},'):gsub(',$', '') )()
end

Context

StackExchange Code Review Q#61135, answer score: 4

Revisions (0)

No revisions yet.