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