diff --git a/src/argparse.lua b/src/argparse.lua index c2fece2..d93bcf3 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -2,34 +2,25 @@ local argparse = {} local class = require "30log" -local State = require "argparse.state" -local utils = require "argparse.utils" +local State = class { + context = {}, -- {alias -> element} + result = {} +} -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 = {} +function State:__init(parser) + self:switch(parser) end -function Parser:add_alias(element, alias) - table.insert(element.aliases, alias) - self.context[alias] = element -end +function State:switch(parser) + self.parser = parser + self.parser:make_targets() -function Parser:apply_options(element, options) - for k, v in pairs(options) do - element[k] = v -- fixme + for _, option in ipairs(parser.options) do + table.insert(self.options, option) end + + self.arguments = parser.arguments + self.commands = parser.commands end function Parser:make_target(element) @@ -45,151 +36,297 @@ function Parser:make_target(element) 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, ...) +function State:parse(args) + args = args or arg - if type(argument) == "string" then - self:add_alias(element, argument) + + +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:apply_options(element, argument) + self._current = nil end end - self:make_target(element) - - element.mincount, element.maxcount = utils.parse_boundaries(element.count) - element.minargs, element.maxargs = utils.parse_boundaries(element.args) - - table.insert(self.arguments, element) - table.insert(self.elements, element) - - return element + 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 Parser:option(name, ...) - local element = { - name = name, - aliases = {}, - count = "0-1", - args = 1, - type = "option" - } +function State:get_result() + self:_check() - self:add_alias(element, name) + local result = {} - local argument - for i = 1, select('#', ...) do - argument = select(i, ...) + local invocations + for _, element in ipairs(self._all_elements) do + invocations = self._result[element.target] - if type(argument) == "string" then - self:add_alias(element, argument) + if element.maxcount == 1 then + if element.maxargs == 0 then + if #invocations > 0 then + result[element.target] = true + end + elseif element.maxargs == 1 and element.minargs == 1 then + if #invocations > 0 then + result[element.target] = invocations[1][1] + end + else + result[element.target] = invocations[1] + end else - self:apply_options(element, argument) + 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 - self:make_target(element) - - element.mincount, element.maxcount = utils.parse_boundaries(element.count) - element.minargs, element.maxargs = utils.parse_boundaries(element.args) - - table.insert(self.elements, element) - - return element + return result end --- DRY? +function State:_check() + self:_assert(not self._parser.must_command, "a command is required") -function Parser:flag(name, ...) - local element = { - name = name, - aliases = {}, - count = "0-1", - args = 0, - type = "option" - } + local invocations + for _, element in ipairs(self._all_elements) do + invocations = self._result[element.target] or {} - self:add_alias(element, name) + if element.type == "argument" and #invocations == 0 then + invocations[1] = {} + end - local argument - for i = 1, select('#', ...) do - argument = select(i, ...) + if #invocations > element.maxcount then + if element.no_overwrite then + self:_error("option %s must be used at most %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 - if type(argument) == "string" then - self:add_alias(element, argument) - else - self:apply_options(element, argument) + 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, "too many arguments") + if #passed < element.minargs then + if element.default then + for i = 1, element.minargs-#passed do + table.insert(passed, element.default) + end + else + self:_error("too few arguments") + end + end + 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 + + +local utils = require "argparse.utils" + +local Declarative = {} + +function Declarative:__init(...) + return self(...) +end + +function Declarative:__call(...) + local name_or_options + + for i=1, select("#", ...) do + name_or_options = select(i, ...) + + if type(name_or_options) == "string" then + self:set_name(name_or_options) + elseif type(name_or_options) == "table" then + for _, field in ipairs(self.fields) do + if name_or_options[field] ~= nil then + self[field] = name_or_options[field] + end + end end end - self:make_target(element) - - element.mincount, element.maxcount = utils.parse_boundaries(element.count) - element.minargs, element.maxargs = utils.parse_boundaries(element.args) - - table.insert(self.elements, element) - - return element + return self end -function Parser:command(name, ...) - local element = { - name = name, - aliases = {}, - count = "0-1", - args = 0, - type = "command" - } +local Aliased = {} - local command = Parser() -- fixme - for k, v in pairs(element) do - command[k] = v +function Aliased:set_name(name) + table.insert(self.aliases, name) + + if not self.name then + self.name = name end +end - self:add_alias(command, name) +local Named = {} - local argument - for i = 1, select('#', ...) do - argument = select(i, ...) - self:add_alias(command, argument) - end +function Named:set_name(name) + self.name = name +end - self:make_target(command) +local Parser = class { + arguments = {}, + options = {}, + commands = {}, + charset = {"-"}, + fields = {"name", "description", "target"} +}:include(Declarative):include(Named) - command.mincount, command.maxcount = utils.parse_boundaries(command.count) - command.minargs, command.maxargs = utils.parse_boundaries(command.args) +local Command = Parser:extends { + aliases = {} +}:include(Declarative):include(Aliased) - table.insert(self.elements, command) +local Argument = class { + args = 1, + count = 1, + fields = {"name", "description", "target", "args", "default", "convert"} +}:include(Declarative):include(Named) +local Option = class { + aliases = {}, + args = 1, + count = "?", + fields = {"name", "aliases", "description", "target", "args", "count", "default", "convert"} +}:include(Declarative):include(Aliased) + +local Flag = Option:extends { + args = 0 +}:include(Declarative):include(Aliased) + +function Parser:argument(...) + local argument = Argument(...) + table.insert(self.arguments, argument) + return argument +end + +function Parser:option(...) + local option = Option(...) + table.insert(self.options, option) + return option +end + +function Parser:flag(...) + local flag = Flag(...) + table.insert(self.options, flag) + return flag +end + +function Parser:command(...) + local command = Command(...) + table.insert(self.commands, 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") @@ -200,6 +337,51 @@ function Parser:assert(assertion, ...) return assertion or self:error(...) end +function Parser:get_charset() + for _, command in ipairs(self.commands) do + for char in command:get_charset() do + self.charset[char] = true + end + end + + for _, option in ipairs(self.options) do + for _, alias in ipairs(option.aliases) do + self.charset[alias:sub(1, 1)] = true + end + end + + return self.charset +end + +-- to be called from State +function Parser:make_targets() + for _, option in ipairs(self.options) do + if not option.target then + for _, alias in ipairs(option.aliases) do + if alias:sub(1, 1) == alias:sub(2, 2) then + option.target = alias:sub(3) + break + end + end + end + + option.target = option.target or option.aliases[1]:sub(2) + end + + for _, argument in ipairs(self.arguments) do + argument.target = argument.target or argument.name + end + + for _, command in ipairs(self.commands) do + command.target = command.target or command.name + end +end + +function Parser:parse(args) + self:get_charset() + return State(self):parse(args) +end + function Parser:parse(args) args = args or arg diff --git a/src/interface.lua b/src/interface.lua new file mode 100644 index 0000000..98322f0 --- /dev/null +++ b/src/interface.lua @@ -0,0 +1,101 @@ +-- new, awesome declarative interface implementation + +local class = require "30log" + +local Declarative = class() + +function Declarative:__init(...) + return self(...) +end + +function Declarative:__call(...) + local name_or_options + + for i=1, select("#", ...) do + name_or_options = select(i, ...) + + if type(name_or_options) == "string" then + self:set_name(name_or_options) + elseif type(name_or_options) == "table" then + for _, field in ipairs(self.fields) do + if name_or_options[field] ~= nil then + self[field] = name_or_options[field] + end + end + end + end + + return self +end + +local Aliased = {} + +function Aliased:set_name(name) + table.insert(self.aliases, name) + + if not self.name then + self.name = name + end +end + +local Named = {} + +function Named:set_name(name) + self.name = name +end + +local Parser = class { + arguments = {}, + options = {}, + commands = {}, + groups = {}, + mutex_groups = {}, + fields = {"name", "description", "target"} +}:include(Declarative):include(Named) + +local Command = Parser:extends { + aliases = {} +}:include(Declarative):include(Aliased) + +local Argument = class { + args = 1, + count = 1, + fields = {"name", "description", "target", "args", "default", "convert"} +}:include(Declarative):include(Named) + +local Option = class { + aliases = {}, + args = 1, + count = "?", + fields = {"name", "aliases", "description", "target", "args", "count", "default", "convert"} +}:include(Declarative):include(Aliased) + +local Flag = Option:extends { + args = 0 +}:include(Declarative):include(Aliased) + +function Parser:argument(...) + local argument = Argument(...) + table.insert(self.arguments, argument) + return argument +end + +function Parser:option(...) + local option = Option(...) + table.insert(self.options, option) + return option +end + +function Parser:flag(...) + local flag = Flag(...) + table.insert(self.options, flag) + return flag +end + +function Parser:command(...) + local command = Command(...) + table.insert(self.commands, command) + return command +end + +return Parser diff --git a/src/interface_test.lua b/src/interface_test.lua new file mode 100644 index 0000000..b56dd29 --- /dev/null +++ b/src/interface_test.lua @@ -0,0 +1,39 @@ +--Just some testing + +local MetaParser = require "interface" + +local parser = MetaParser "luarocks" { + description = "a module deployment system for Lua" +} + +parser:option "--server" "-s" { + description = "Fetch rocks/rockspecs from this server" +} + +parser:flag "--local" "-l" { + description = "Use the tree in the user's home directory." +} + +local install = parser:command "install" "i" + +install:argument "rock" + +install:argument "version" { + args = "?" +} + +assert(parser.description == "a module deployment system for Lua") +assert(parser.options[1].name == "--server") +assert(parser.options[1].aliases[1] == "--server") +assert(parser.options[1].aliases[2] == "-s") +assert(parser.options[1].description == "Fetch rocks/rockspecs from this server") +assert(parser.options[1].args == 1) +assert(parser.options[1].count == "?") +assert(parser.options[2].name == "--local") +assert(parser.options[2].args == 0) +assert(parser.commands[1] == install) +assert(install.arguments[1].name == "rock") +assert(install.aliases[2] == "i") +assert(install.arguments[2].name == "version") +assert(install.arguments[2].count == 1) +assert(install.arguments[2].args == "?") diff --git a/src/state.lua b/src/state.lua index 7f52470..4f0f9ff 100644 --- a/src/state.lua +++ b/src/state.lua @@ -1,6 +1,8 @@ local class = require "30log" -local State = class() +local State = class { + context = {} +} function State:__init(parser) self.context = {}