lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]




On 07/04/15 09:12 PM, Hisham wrote:
Hi all,

So here's a little problem that some of you might enjoy playing with
in case you're bored.

Let's play "variable expansion". We want to convert "Hello, $planet!"
to "Hello, Earth!" given { planet = "Earth" }, but we also want it to
support "Hello, ${planet}!".

That is, we want to perform "variable expansion" in a string s, so
that given a table of "variables" we want to expand every $foo or
${foo} in s to the value of variables.foo.

HOWEVER, we don't want recursive expansion (ie, if the value of
variables.foo is "$bar", then we _don't_ want it expanded to the value
of variables.bar).

For simplicity, assume that variable names are in the form [A-Za-z0-9_]+

Here's a test script for you to play with:

-----------------------------------------
local variables = {
    foo = "$bar",
    foo_bar = "blabla",
    wee = "${foo}",
    bar = "foo",
}

local function expand_variables(str)
    -- TODO
end

local tests = {
    { from = "hello $foo", to = "hello $bar" },
    { from = "hello ${foo}", to = "hello $bar" },
    { from = "hello $wee", to = "hello ${foo}" },
    { from = "hello$foo_bar $foo", to = "helloblabla $bar" },
    { from = "hel${bar}lo", to = "helfoolo" },
    { from = "hel${foo_bar}lo", to = "helblablalo" },
    { from = "${foo_bar}", to = "blabla" },
    { from = "end${bar}", to = "endfoo" },
    { from = "${bar}end", to = "fooend" },
    { from = "end$foo", to = "end$bar" },
    -- At your discretion: how to deal with...
    -- "$fooend"
    -- "${}"
    -- "${bo{bozo}zo}"
    -- "${bo${bar}zo}"
}

for _, test in ipairs(tests) do
    local exp = expand_variables(test.from)
    if exp == test.to then
       print("OK", test.from, exp)
    else
       print("ERROR", test.from, test.to, exp)
    end
end
-----------------------------------------

Note that the naive approach of chaining two str:gsub() calls like
this won't work:

expand_variables = function(str)
    return str:gsub("%$([A-Za-z0-9_]+)",
variables):gsub("%${([A-Za-z0-9_]+)}", variables)
end

expand_variables = function(str)
   return str:gsub("%$({?)([A-Za-z0-9_]+)(}?)", function(a,b,c)
      -- little trick with the lengths, if a matches then it'll be 1, if c
      -- doesn't match it'll be 0 and 1 <= 0 is false, thus returning false
      -- and keeping the match as-is. for both matching, 1 <= 1 is true, so
      -- you get variables[b]. for neither matching, 0 <= 0 is true, so you
      -- get variables[b].
      -- ps: you never asked us to handle "$foo}" so I decided to eat the }
      return #a <= #c and variables[b]
   end)
end



...because this will make the second or third test case fail
(depending on which you expand first).

My first attempt at doing this was the following:

local function expand_variables(str)
    return str:gsub("%$([A-Za-z0-9_{}]+)", function(cap)
       local br, rest = cap:match("^{([^{}]+)}(.*)")
       if br then return (variables[br] or "") .. rest end
       local nobr, rest = cap:match("^([^{}]+)(.*)")
       return (variables[nobr] or "") .. (rest or "")
    end)
end

which I then slightly changed to the following because of the test
cases commented above:

local function expand_variables(str)
    return str:gsub("%$([A-Za-z0-9_{}]+)", function(cap)
       local br, rest = cap:match("^{([^{}]+)}(.*)")
       if br then return (variables[br] or "") .. rest end
       local nobr, rest = cap:match("^([^{}]+)(.*)")
       if nobr then return (variables[nobr] or "") .. rest end
       return "$"..cap
    end)
end

Can you suggest any nicer and/or shorter solution?

Ah! NO LPEG ALLOWED! :)

Cheers,

-- Hisham


--
Disclaimer: these emails are public and can be accessed from <TODO: get a non-DHCP IP and put it here>. If you do not agree with this, DO NOT REPLY.