123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- ChatCmdBuilder = {}
- function ChatCmdBuilder.new(name, func, def)
- def = def or {}
- local cmd = ChatCmdBuilder.build(func)
- cmd.def = def
- def.func = cmd.run
- minetest.register_chatcommand(name, def)
- return cmd
- end
- local STATE_READY = 1
- local STATE_PARAM = 2
- local STATE_PARAM_TYPE = 3
- local bad_chars = {}
- bad_chars["("] = true
- bad_chars[")"] = true
- bad_chars["."] = true
- bad_chars["%"] = true
- bad_chars["+"] = true
- bad_chars["-"] = true
- bad_chars["*"] = true
- bad_chars["?"] = true
- bad_chars["["] = true
- bad_chars["^"] = true
- bad_chars["$"] = true
- local function escape(char)
- if bad_chars[char] then
- return "%" .. char
- else
- return char
- end
- end
- local dprint = function() end
- ChatCmdBuilder.types = {
- pos = "%(? *(%-?[%d.]+) *, *(%-?[%d.]+) *, *(%-?[%d.]+) *%)?",
- text = "(.+)",
- number = "(%-?[%d.]+)",
- int = "(%-?[%d]+)",
- word = "([^ ]+)",
- alpha = "([A-Za-z]+)",
- modname = "([a-z0-9_]+)",
- alphascore = "([A-Za-z_]+)",
- alphanumeric = "([A-Za-z0-9]+)",
- username = "([A-Za-z0-9-_]+)",
- }
- function ChatCmdBuilder.build(func)
- local cmd = {
- _subs = {}
- }
- function cmd:sub(route, func, def)
- dprint("Parsing " .. route)
- def = def or {}
- if string.trim then
- route = string.trim(route)
- end
- local sub = {
- pattern = "^",
- params = {},
- func = func
- }
- -- End of param reached: add it to the pattern
- local param = ""
- local param_type = ""
- local should_be_eos = false
- local function finishParam()
- if param ~= "" and param_type ~= "" then
- dprint(" - Found param " .. param .. " type " .. param_type)
- local pattern = ChatCmdBuilder.types[param_type]
- if not pattern then
- error("Unrecognised param_type=" .. param_type)
- end
- sub.pattern = sub.pattern .. pattern
- table.insert(sub.params, param_type)
- param = ""
- param_type = ""
- end
- end
- -- Iterate through the route to find params
- local state = STATE_READY
- local catching_space = false
- local match_space = " " -- change to "%s" to also catch tabs and newlines
- local catch_space = match_space.."+"
- for i = 1, #route do
- local c = route:sub(i, i)
- if should_be_eos then
- error("Should be end of string. Nothing is allowed after a param of type text.")
- end
- if state == STATE_READY then
- if c == ":" then
- dprint(" - Found :, entering param")
- state = STATE_PARAM
- param_type = "word"
- catching_space = false
- elseif c:match(match_space) then
- print(" - Found space")
- if not catching_space then
- catching_space = true
- sub.pattern = sub.pattern .. catch_space
- end
- else
- catching_space = false
- sub.pattern = sub.pattern .. escape(c)
- end
- elseif state == STATE_PARAM then
- if c == ":" then
- dprint(" - Found :, entering param type")
- state = STATE_PARAM_TYPE
- param_type = ""
- elseif c:match(match_space) then
- print(" - Found whitespace, leaving param")
- state = STATE_READY
- finishParam()
- catching_space = true
- sub.pattern = sub.pattern .. catch_space
- elseif c:match("%W") then
- dprint(" - Found nonalphanum, leaving param")
- state = STATE_READY
- finishParam()
- sub.pattern = sub.pattern .. escape(c)
- else
- param = param .. c
- end
- elseif state == STATE_PARAM_TYPE then
- if c:match(match_space) then
- print(" - Found space, leaving param type")
- state = STATE_READY
- finishParam()
- catching_space = true
- sub.pattern = sub.pattern .. catch_space
- elseif c:match("%W") then
- dprint(" - Found nonalphanum, leaving param type")
- state = STATE_READY
- finishParam()
- sub.pattern = sub.pattern .. escape(c)
- else
- param_type = param_type .. c
- end
- end
- end
- dprint(" - End of route")
- finishParam()
- sub.pattern = sub.pattern .. "$"
- dprint("Pattern: " .. sub.pattern)
- table.insert(self._subs, sub)
- end
- if func then
- func(cmd)
- end
- cmd.run = function(name, param)
- for i = 1, #cmd._subs do
- local sub = cmd._subs[i]
- local res = { string.match(param, sub.pattern) }
- if #res > 0 then
- local pointer = 1
- local params = { name }
- for j = 1, #sub.params do
- local param = sub.params[j]
- if param == "pos" then
- local pos = {
- x = tonumber(res[pointer]),
- y = tonumber(res[pointer + 1]),
- z = tonumber(res[pointer + 2])
- }
- table.insert(params, pos)
- pointer = pointer + 3
- elseif param == "number" or param == "int" then
- table.insert(params, tonumber(res[pointer]))
- pointer = pointer + 1
- else
- table.insert(params, res[pointer])
- pointer = pointer + 1
- end
- end
- if table.unpack then
- -- lua 5.2 or later
- return sub.func(table.unpack(params))
- else
- -- lua 5.1 or earlier
- return sub.func(unpack(params))
- end
- end
- end
- return false, "Invalid command"
- end
- return cmd
- end
- local function run_tests()
- if not (ChatCmdBuilder.build(function(cmd)
- cmd:sub("bar :one and :two:word", function(name, one, two)
- if name == "singleplayer" and one == "abc" and two == "def" then
- return true
- end
- end)
- end)).run("singleplayer", "bar abc and def") then
- error("Test 1 failed")
- end
- local move = ChatCmdBuilder.build(function(cmd)
- cmd:sub("move :target to :pos:pos", function(name, target, pos)
- if name == "singleplayer" and target == "player1" and
- pos.x == 0 and pos.y == 1 and pos.z == 2 then
- return true
- end
- end)
- end).run
- if not move("singleplayer", "move player1 to 0,1,2") then
- error("Test 2 failed")
- end
- if not move("singleplayer", "move player1 to (0,1,2)") then
- error("Test 3 failed")
- end
- if not move("singleplayer", "move player1 to 0, 1,2") then
- error("Test 4 failed")
- end
- if not move("singleplayer", "move player1 to 0 ,1, 2") then
- error("Test 5 failed")
- end
- if not move("singleplayer", "move player1 to 0, 1, 2") then
- error("Test 6 failed")
- end
- if not move("singleplayer", "move player1 to 0 ,1 ,2") then
- error("Test 7 failed")
- end
- if not move("singleplayer", "move player1 to ( 0 ,1 ,2)") then
- error("Test 8 failed")
- end
- if move("singleplayer", "move player1 to abc,def,sdosd") then
- error("Test 9 failed")
- end
- if move("singleplayer", "move player1 to abc def sdosd") then
- error("Test 10 failed")
- end
- if not (ChatCmdBuilder.build(function(cmd)
- cmd:sub("does :one:int plus :two:int equal :three:int", function(name, one, two, three)
- if name == "singleplayer" and one + two == three then
- return true
- end
- end)
- end)).run("singleplayer", "does 1 plus 2 equal 3") then
- error("Test 11 failed")
- end
- local checknegint = ChatCmdBuilder.build(function(cmd)
- cmd:sub("checknegint :x:int", function(name, x)
- return x
- end)
- end).run
- if checknegint("checker","checknegint -2") ~= -2 then
- error("Test 12 failed")
- end
- local checknegnumber = ChatCmdBuilder.build(function(cmd)
- cmd:sub("checknegnumber :x:number", function(name, x)
- return x
- end)
- end).run
- if checknegnumber("checker","checknegnumber -3.3") ~= -3.3 then
- error("Test 13 failed")
- end
- local checknegpos = ChatCmdBuilder.build(function(cmd)
- cmd:sub("checknegpos :pos:pos", function(name, pos)
- return pos
- end)
- end).run
- local negpos = checknegpos("checker","checknegpos (-13.3,-4.6,-1234.5)")
- if negpos.x ~= -13.3 or negpos.y ~= -4.6 or negpos.z ~= -1234.5 then
- error("Test 14 failed")
- end
- local checktypes = ChatCmdBuilder.build(function(cmd)
- cmd:sub("checktypes :int:int :number:number :pos:pos :word:word :text:text", function(name, int, number, pos, word, text)
- return int, number, pos.x, pos.y, pos.z, word, text
- end)
- end).run
- local int, number, posx, posy, posz, word, text
- int, number, posx, posy, posz, word, text = checktypes("checker","checktypes -1 -2.4 (-3,-5.3,6.12) some text to finish off with")
- --dprint(int, number, posx, posy, posz, word, text)
- if int ~= -1 or number ~= -2.4 or posx ~= -3 or posy ~= -5.3 or posz ~= 6.12 or word ~= "some" or text ~= "text to finish off with" then
- error("Test 15 failed")
- end
- dprint("All tests passed")
- end
- if not minetest then
- run_tests()
- end
|