Inserting quoted and unquoted parts of a row into a table
I have been working on this part of the saycommand system, which is to strip parts of a string and put them in a table that is sent to a function that is requested at the beginning of the string. It will look like, for example, !save 1
or !teleport 0 1
, or !tell 5 "a private message"
.
I would like this row to turn into a table:
[[1 2 word 2 9 'more words' 1 "and more" "1 2 34"]]
(Each unrolled part of the string gets its own key, and the quoted parts are grouped into a key)
1 = 1
2 = 2
3 = word
4 = 2
5 = 9
6 = more words
7 = 1
8 = and more
9 = 1 2 34
I've tried doing this with the Lua pattern, but I'm stuck trying to figure out how to capture both quoted and unquoted chunks of a string. I've tried a lot of things but nothing seems to work.
My current templating attempts look like this:
a, d = '1 2 word 2 9 "more words" 1 "and more" "1 2 34"" ', {}
-- previous attempts
--[[
This one captures quotes
a:gsub('(["\'])(.-)%1', function(a, b) table.insert(d, b) end)
This one captures some values and butchered quotes,
which might have to do with spaces in the string
a:gsub('(["%s])(.-)%1', function(a, b) table.insert(d, b) end)
This one captures every value, but doesn't take care of quotes
a:gsub('(%w+)', function(a) table.insert(d, a) end)
This one tries making %s inside of quotes into underscores to
ignore them there, but it doesn't work
a = a:gsub('([%w"\']+)', '%1_')
a:gsub('(["\'_])(.-)%1', function(a, b) table.insert(d, b) end)
a:gsub('([%w_]+)', function(a) table.insert(d, a) end)
This one was a wild attempt at cracking it, but no success
a:gsub('["\']([^"\']-)["\'%s]', function(a) table.insert(d, a) end)
--]]
-- This one adds spaces, which would later be trimmed off, to test
-- whether it helped with the butchered strings, but it doesn't
a = a:gsub('(%w)(%s)(%w)', '%1%2%2%3')
a:gsub('(["\'%s])(.-)%1', function(a, b) table.insert(d, b) end)
for k, v in pairs(d) do
print(k..' = '..v)
end
It won't be needed for simple commands, but a more complex one, for example !tell 1 2 3 4 5 "a private message sent to five people"
, needs it, first check if it's sent to multiple people and then to find out what the message is.
Further down the line, I want to add commands such as !give 1 2 3 "component:material_iron:weapontype" "food:calories"
, which should add two items to three different people, would greatly benefit from such a system.
If this is not possible in a Lua pattern, I will try to do it with loops and so on, but I really feel like I am missing something obvious. May I say this?
source to share
You cannot handle quoted strings with Lua templates. You need to explicitly parse the string like in the code below.
function split(s)
local t={}
local n=0
local b,e=0,0
while true do
b,e=s:find("%s*",e+1)
b=e+1
if b>#s then break end
n=n+1
if s:sub(b,b)=="'" then
b,e=s:find(".-'",b+1)
t[n]=s:sub(b,e-1)
elseif s:sub(b,b)=='"' then
b,e=s:find('.-"',b+1)
t[n]=s:sub(b,e-1)
else
b,e=s:find("%S+",b)
t[n]=s:sub(b,e)
end
end
return t
end
s=[[1 2 word 2 9 'more words' 1 "and more" "1 2 34"]]
print(s)
t=split(s)
for k,v in ipairs(t) do
print(k,v)
end
source to share
Lua string patterns and regex are generally not very suitable when you need to do parsing that requires varying levels of nesting or counter balancing, such as parentheses ( )
. But there is another tool for Lua that is powerful enough to handle this requirement: LPeg .
The LPeg syntax is a bit archaic and requires some use, so I'll use the lpeg module re
to make it easier to digest. Keep in mind that anything you can do in one syntax form, you can also express it in another form.
I'll start by defining a grammar to parse your format description:
local re = require 're'
local cmdgrammar =
[[
saycmd <- '!' cmd extra
cmd <- %a%w+
extra <- (singlequote / doublequote / unquote / .)*
unquote <- %w+
singlequote <- "'" (unquote / %s)* "'"
doublequote <- '"' (unquote / %s)* '"'
]]
Then, compile the grammar and use it to match some of your test cases:
local cmd_parser = re.compile(cmdgrammar)
local saytest =
{
[[!save 1 2 word 2 9 'more words' 1 "and more" "1 2 34"]],
[[!tell 5 "a private message"]],
[[!teleport 0 1]],
[[!say 'another private message' 42 "foo bar" baz]],
}
There are currently no entries in the grammar, so it re.match
returns the last character position in a string that was able to match up to + 1. This means that a successful parsing will return the full character count of the string + 1 and is therefore a valid example of your grammar.
for _, test in ipairs(saytest) do
assert(cmd_parser:match(test) == #test + 1)
end
Now comes the fun part. Once you've worked your way around the grammar you want, you can now add captures that automatically fetch the results you want into the lua table with relatively little effort. This displays the resulting spec + grammar table:
local cmdgrammar =
[[
saycmd <- '!' {| {:cmd: cmd :} {:extra: extra :} |}
cmd <- %a%w+
extra <- {| (singlequote / doublequote / { unquote } / .)* |}
unquote <- %w+
singlequote <- "'" { (unquote / %s)* } "'"
doublequote <- '"' { (unquote / %s)* } '"'
]]
Restarting tests and resetting results re.match
:
for i, test in ipairs(saytest) do
print(i .. ':')
dump(cmd_parser:match(test))
end
You should get a result similar to:
lua say.lua
1:
{
extra = {
"1",
"2",
"word",
"2",
"9",
"more words",
"1",
"and more",
"1 2 34"
},
cmd = "save"
}
2:
{
extra = {
"5",
"a private message"
},
cmd = "tell"
}
3:
{
extra = {
"0",
"1"
},
cmd = "teleport"
}
4:
{
extra = {
"another private message",
"42",
"foo bar",
"baz"
},
cmd = "say"
}
source to share