diff --git a/rockspecs/largparse-0.1-1.rockspec~ b/rockspecs/largparse-0.1-1.rockspec~ new file mode 100644 index 0000000..b93aae8 --- /dev/null +++ b/rockspecs/largparse-0.1-1.rockspec~ @@ -0,0 +1,22 @@ +package = "largparse" +version = "git-1" +source = { + url = "src" +} +description = { + summary = "*** please specify description summary ***", + detailed = "*** please enter a detailed description ***", + homepage = "*** please enter a project homepage ***", + license = "MIT/X11" +} +dependencies = { + "lua >= 5.1, < 5.3", + "30log >= 0.6" +} +build = { + type = "builtin", + modules = { + largparse = "src/largparse.lua", + ["largparse.state"] = "src/state.lua" + } +} diff --git a/rockspecs/largparse-wip-1.rockspec b/rockspecs/largparse-wip-1.rockspec new file mode 100644 index 0000000..a3323e4 --- /dev/null +++ b/rockspecs/largparse-wip-1.rockspec @@ -0,0 +1,22 @@ +package = "largparse" +version = "wip-1" +source = { + url = "src" +} +description = { + summary = "*** please specify description summary ***", + detailed = "*** please enter a detailed description ***", + homepage = "*** please enter a project homepage ***", + license = "MIT/X11" +} +dependencies = { + "lua >= 5.1, < 5.3", + "30log >= 0.6" +} +build = { + type = "builtin", + modules = { + largparse = "src/largparse.lua", + ["largparse.state"] = "src/state.lua" + } +} diff --git a/src/largparse.lua b/src/largparse.lua new file mode 100644 index 0000000..1ea46b1 --- /dev/null +++ b/src/largparse.lua @@ -0,0 +1,297 @@ +local largparse = {} + +local class = require "30log" + +local State = require "largparse.state" + +local Parser = class() + +function Parser:__init(options) + options = options or {} + self.description = options.description + self.name = options.name + self.no_help = options.no_help + self.must_command = options.must_command + + self.arguments = {} + self.elements = {} + self.groups = {} + self.commands = {} + self.context = {} +end + +function Parser:add_alias(element, alias) + table.insert(element.aliases, alias) + self.context[alias] = element +end + +function Parser:apply_options(element, options) + for k, v in pairs(options) do + element[k] = v -- fixme + end +end + +function Parser:make_target(element) + if not element.target then + for _, alias in ipairs(element.aliases) do + if alias:match "^%-%-" then + element.target = alias:sub(3) + return + end + end + + element.target = element.aliases[1]:match "^%-*(.*)" + end +end + +function Parser:parse_boundaries(boundaries) + if tonumber(boundaries) then + return tonumber(boundaries), tonumber(boundaries) + end + + if boundaries == "*" then + return 0, math.huge + end + + if boundaries == "+" then + return 1, math.huge + end + + if boundaries == "?" then + return 0, 1 + end + + if boundaries:match "^%d+%-%d+$" then + local min, max = boundaries:match "^(%d+)%-(%d+)$" + return tonumber(min), tonumber(max) + end + + if boundaries:match "^%d+%+$" then + local min = boundaries:match "^(%d+)%+$" + return tonumber(min), math.huge + end +end + +-- TODO: make it declarative as it was +function Parser:argument(name, ...) + local element = { + name = name, + aliases = {}, + count = 1, + args = 1, + type = "argument" + } + + self:add_alias(element, name) + + local argument + for i = 1, select('#', ...) do + argument = select(i, ...) + + if type(argument) == "string" then + self:add_alias(element, argument) + else + self:apply_options(element, argument) + end + end + + self:make_target(element) + + element.mincount, element.maxcount = self:parse_boundaries(element.count) + element.minargs, element.maxargs = self:parse_boundaries(element.args) + if element.minargs == 0 then + element.mincount = 0 + end + + table.insert(self.arguments, element) + table.insert(self.elements, element) + + return element +end + +function Parser:option(name, ...) + local element = { + name = name, + aliases = {}, + count = "0-1", + args = 1, + type = "option" + } + + self:add_alias(element, name) + + local argument + for i = 1, select('#', ...) do + argument = select(i, ...) + + if type(argument) == "string" then + self:add_alias(element, argument) + else + self:apply_options(element, argument) + end + end + + self:make_target(element) + + element.mincount, element.maxcount = self:parse_boundaries(element.count) + element.minargs, element.maxargs = self:parse_boundaries(element.args) + + table.insert(self.elements, element) + + return element +end + +-- DRY? + +function Parser:flag(name, ...) + local element = { + name = name, + aliases = {}, + count = "0-1", + args = 0, + type = "option" + } + + self:add_alias(element, name) + + local argument + for i = 1, select('#', ...) do + argument = select(i, ...) + + if type(argument) == "string" then + self:add_alias(element, argument) + else + self:apply_options(element, argument) + end + end + + self:make_target(element) + + element.mincount, element.maxcount = self:parse_boundaries(element.count) + element.minargs, element.maxargs = self:parse_boundaries(element.args) + + table.insert(self.elements, element) + + return element +end + +function Parser:command(name, ...) + local element = { + name = name, + aliases = {}, + count = "0-1", + args = 0, + type = "command" + } + + local command = Parser() -- fixme + for k, v in pairs(element) do + command[k] = v + end + + self:add_alias(command, name) + + local argument + for i = 1, select('#', ...) do + argument = select(i, ...) + self:add_alias(command, argument) + end + + self:make_target(command) + + command.mincount, command.maxcount = self:parse_boundaries(command.count) + command.minargs, command.maxargs = self:parse_boundaries(command.args) + + table.insert(self.elements, command) + + return command +end + +function Parser:group(...) + -- +end + +function Parser:mutually_exclusive(...) + local group = { + elements = {...} + } + + table.insert(self.groups, group) + return group +end + +function Parser:error(fmt, ...) + local msg = fmt:format(...) + io.stderr:write("Error: " .. msg .. "\n") + os.exit(1) +end + +function Parser:assert(assertion, ...) + return assertion or self:error(...) +end + + +function Parser:parse(args) + args = args or arg + self.name = self.name or args[0] + + local state = State(self) + + local handle_options = true + for _, data in ipairs(args) do + local plain = true + + if handle_options then + if data:sub(1, 1) == "-" then + if #data > 1 then + if data:sub(2, 2):match "[a-zA-Z]" then + plain = false + + local name, element + for i = 2, #data do + name = "-" .. data:sub(i, i) + element = self:assert(state.context[name], "unknown option " .. name) + state:handle_option(name) + + if i ~= #data and not (element:can_take(0) and data:sub(i+1, i+1):match "[a-zA-Z]") then + state:handle_argument(data:sub(i+1)) + break + end + end + elseif data:sub(2, 2) == "-" then + if #data == 2 then + plain = false + handle_options = false + elseif data:sub(3, 3):match "[a-zA-Z]" then + plain = false + + local equal = data:find "=" + if equal then + local name = data:sub(1, equal-1) + local element = self:assert(state.context[name], "unknown option " .. name) + self:assert(element.maxargs > 0, "option " .. name .. " doesn't take arguments") + + state:handle_option(data:sub(1, equal-1)) + state:handle_argument(data:sub(equal+1)) + else + state:handle_option(data) + end + end + end + end + end + end + + if plain then + state:handle_argument(data) + end + end + + local result = state:get_result() + + return result +end + +largparse.parser = Parser + +return largparse diff --git a/src/state.lua b/src/state.lua new file mode 100644 index 0000000..5a6ab9b --- /dev/null +++ b/src/state.lua @@ -0,0 +1,185 @@ +local class = require "30log" + +local State = class() + +function State:__init(parser) + self.context = {} + self._all_elements = {} + self._all_groups = {} + self:_switch(parser) + + self._result = {} +end + +function State:handle_option(name) + local option = self:_assert(self.context[name], "unknown option %s", name) + + self:_open(option) +end + +function State:handle_argument(data) + if self._current then + if self:_can_pass(self._current) then + self:_pass(self._current, data) + return + else + self._current = nil + end + end + + local argument = self._arguments[self._next_arg_i] + if argument then + self:_open(argument) + self:_pass(argument, data) + else + local command = self.context[data] + if command and command.type == "command" then + self._result[command.target] = {{}} + self:_switch(command) + else + if #self._commands > 0 then + self:_error("unknown command %s", data) + else + self:_error("too many arguments") + end + end + end +end + +function State:get_result() + self:_check() + + local result = {} + + local invocations + for _, element in ipairs(self._all_elements) do + invocations = self._result[element.target] + + if element.maxcount == 1 then + if #invocations > 0 then + if element.maxargs == 0 then + result[element.target] = true + elseif element.maxargs == 1 and element.minargs == 1 then + result[element.target] = invocations[1][1] + else + result[element.target] = invocations[1] + end + end + else + if element.maxargs == 0 then + result[element.target] = #invocations + elseif element.maxargs == 1 and element.minargs == 1 then + local new_result = {} + for i, passed in ipairs(invocations) do + new_result[i] = passed[1] + end + result[element.target] = new_result + else + result[element.target] = invocations + end + end + end + + return result +end + +function State:_check() + self:_assert(not self._parser.must_command, "a command is required") + + local invocations + for _, element in ipairs(self._all_elements) do + invocations = self._result[element.target] or {} + if #invocations > element.maxcount then + if element.no_overwrite then + self:_error("option %s can only be used %d times", element.name, element.maxcount) + else + local new_invocations = {} + for i = 1, element.maxcount do + new_invocations[i] = invocations[#invocations-element.maxcount+i] + end + invocations = new_invocations + end + end + + self:_assert(#invocations >= element.mincount, "option %s must be used at least %d times", element.name, element.mincount) + + for _, passed in ipairs(invocations) do + self:_assert(#passed <= element.maxargs, "%s takes at most %d arguments", element.name, element.maxargs) + self:_assert(#passed >= element.minargs, "%s takes at least %d arguments", element.name, element.minargs) + end + + self._result[element.target] = invocations + end + + for _, group in ipairs(self._all_groups) do + local invoked + for _, element in ipairs(group.elements) do + if #self._result[element.target] > 0 then + if invoked then + self:_error("%s can not be used together with %s", invoked.name, element.name) + else + invoked = element + end + end + end + + if group.required then + self:_assert(invoked, "WIP(required mutually exclusive group)") + end + end +end + +function State:_open(element) + if not self._result[element.target] then + self._result[element.target] = {} + end + + table.insert(self._result[element.target], {}) + + if element.type == "argument" then + self._next_arg_i = self._next_arg_i+1 + end + + self._current = element +end + +function State:_can_pass(element) + local invocations = self._result[element.target] + local passed = invocations[#invocations] + + return #passed < element.maxargs +end + +function State:_pass(element, data) + local invocations = self._result[element.target] + local passed = invocations[#invocations] + + table.insert(passed, data) +end + +function State:_switch(command) + self._parser = command + self._arguments = command.arguments + self._commands = command.commands + + for _, element in ipairs(command.elements) do + table.insert(self._all_elements, element) + end + + for _, group in ipairs(command.groups) do + table.insert(self._all_groups, group) + end + + self.context = setmetatable(command.context, {__index = self.context}) + self._next_arg_i = 1 +end + +function State:_error(...) + return self._parser:error(...) +end + +function State:_assert(...) + return self._parser:assert(...) +end + +return State diff --git a/test/test.lua b/test/test.lua new file mode 100644 index 0000000..8a754c6 --- /dev/null +++ b/test/test.lua @@ -0,0 +1,24 @@ +local largparse = require "largparse" +local serpent = require "serpent" + +local parser = largparse.parser() + +parser:argument("input", { + args = 2 +}) + +parser:flag("-q", "--quiet") +parser:option("-s", "--server") + +parser:mutually_exclusive( + parser:flag("-q", "--quiet"), + parser:option("-s", "--server") +) + +local run = parser:command "run" + +run:flag("-f", "--fast") + +local args = parser:parse() + +print(serpent.block(args))