--[[ 命令解析器 任何命令需要首先注册其格式才可能被正确的识别。 命令有一个字符串名称,位于所有参数之前。 参数可以任意个数,需要指定其名称和类型,目前支持数字和字符串两种类型,分别使用n和s表示。 格式为:命令 [参数] ... 注册过格式的命令可以被解析方法识别。输入为一条字符串,由两个部分组成:命令、参数。 字符串使用空格来分隔,命令和参数中不可含有空格。 识别不成功会抛出错误,例如:命令无法被匹配、输入参数数量不足、类型转化失败等。 识别成功则返回一个表,包含所有匹配和解析成功的命令和参数。 接口说明: 使用Command.new来创建新命令对象,调用argument/option方法注册参数及格式 使用Parser.parse函数来解析命令,返回值result上有两个方法 result:command() 返回命令本身 result:iterator() 返回迭代器,配合for语句对参数进行遍历 也可以直接使用key来访问某个已知名称的参数 ]] local ArgParser = { TypeNumber = {}, TypeString = {}, } local TypeNumber = ArgParser.TypeNumber local TypeString = ArgParser.TypeString --命令注册表 local _registry = {} --参数信息 local Command = {} local Result = {} local OptionalDefaultValue = {} --error handler local function report_fail(fmt, ...) error(string.format(fmt, ...), 2) end ----------------------------------------------------------- --regist new command function Command.new(commandName) if _registry[commandName] then report_fail('duplicate command: %s', commandName) end local inst = { commandName = commandName, argNames = {}, argTypes = {}, argDefValues = {}, argDescriptions = {}, } _registry[commandName] = inst Command.__index = Command return setmetatable(inst, Command) end function Command:RegistParam(argName, argType, defVal, desc) if defVal == OptionalDefaultValue and #self.argDefValues > 0 and self.argDefValues[#self.argDefValues] ~= OptionalDefaultValue then report_fail('option argument must at the end: %s %s', argName, argType) end table.insert(self.argNames, argName) table.insert(self.argTypes, argType) table.insert(self.argDefValues, defVal) table.insert(self.argDescriptions, string.format('%s%s%s%s', argName, argType == TypeNumber and ',数字' or ',文本', desc == nil and '' or string.format(',%s', desc), defVal == OptionalDefaultValue and '' or string.format(',默认值=%s', defVal) )) end --required argument with typename function Command:RequiredParam(name, typename, desc) if typename == TypeNumber or typename == TypeString then self:RegistParam(name, typename, OptionalDefaultValue, desc) return self else report_fail('invalid argument type: %s %s %s', name, typename, desc) end end --optional argument with default value function Command:OptionalParam(name, default, desc) local tn = type(default) if tn == 'number' then self:RegistParam(name, TypeNumber, default, desc) return self elseif tn == 'string' then self:RegistParam(name, TypeString, default, desc) return self else report_fail('invalid argument type: %s %s %s', name, default, desc) end end ----------------------------------------------------------- local function match_argument(token, typename) if typename == TypeNumber then local val = tonumber(token) if val == nil then report_fail("need number value for argument: %s", token) else return val end elseif typename == TypeString then local val = tostring(token) if val == nil then report_fail("need string value for argument: %s", token) else return val end end end local function append_argument(result, argName, argVal) result[argName] = argVal table.insert(result.argOrder, argName) end local function result_iter(result, index) index = index + 1 local argName = result.argOrder[index] if argName then return index, argName, result[argName] else return nil end end function Result.new(command) local inst = { commandName = command, argOrder = {}, } Result.__index = Result return setmetatable(inst, Result) end function Result:GetCommandName() return self.commandName end function Result:Iterator() return result_iter, self, 0 end ----------------------------------------------------------- function ArgParser.RegistCommand(commandName) return Command.new(commandName) end function ArgParser.GetCommand(cmdName) return _registry[cmdName] end --result functions function ArgParser.ParseResult(cmdStr) local tokens = {} --argument types: string / number (negative or positive, int or float) for token in string.gmatch(cmdStr, "[-./_,%a%d]+") do table.insert(tokens, token) end if #tokens > 0 then local commandName = tokens[1] local commandData = _registry[commandName] if commandData then table.remove(tokens, 1) local result = Result.new(commandData.commandName) for i, argName in ipairs(commandData.argNames) do if i > #tokens then local defVal = commandData.argDefValues[i] if defVal == OptionalDefaultValue then report_fail('%s, positional argument not enough %s', cmdStr, argName) else append_argument(result, argName, defVal) end else local argType = commandData.argTypes[i] local val = match_argument(tokens[i], argType) if val == nil then report_fail("%s, argument type mismatch %s %s", cmdStr, argType, tokens[i]) else append_argument(result, argName, val) end end end return result else report_fail("unknown command: %s", cmdStr) end else report_fail("malformed command: %s", cmdStr) end end -- local function Test() -- local function TestCase(inputStr) -- local ret, result = pcall(ArgParser.parse, inputStr) -- if ret then -- print('command:', result:command()) -- for k,v in result:iterator() do -- print(string.format('%s=%s, %s', k, v, type(v))) -- end -- else -- print(inputStr, 'parse failed,', result) -- end -- end -- Command.new('test') -- Command.new('test2'):required('name', TypeString) -- Command.new('test3'):required('i1', TypeNumber):required('i2', TypeNumber):optional('str', 'foo') -- TestCase('test') --匹配成功 -- TestCase('foo') --匹配失败,命令无法识别 -- TestCase('test 3.2 abc') --匹配成功,多余的参数被忽略 -- TestCase('test2 tom') --匹配成功 -- TestCase('test3 10 0.5 xyz') --匹配成功 -- TestCase('test3 10 abc xyz') --匹配失败,参数需要数字来 -- TestCase('test3 10 0.5') --匹配成功,可选参数使用默认值 -- TestCase('test3 10') --匹配失败,参数数量不足 -- end -- Test() return ArgParser