declarative interface

This commit is contained in:
mpeterv
2014-01-15 12:13:54 +04:00
parent 1a52acca75
commit a7f7edbb3e
4 changed files with 455 additions and 131 deletions

View File

@@ -2,34 +2,25 @@ local argparse = {}
local class = require "30log" local class = require "30log"
local State = require "argparse.state" local State = class {
local utils = require "argparse.utils" context = {}, -- {alias -> element}
result = {}
}
local Parser = class() function State:__init(parser)
self:switch(parser)
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 end
function Parser:add_alias(element, alias) function State:switch(parser)
table.insert(element.aliases, alias) self.parser = parser
self.context[alias] = element self.parser:make_targets()
end
function Parser:apply_options(element, options) for _, option in ipairs(parser.options) do
for k, v in pairs(options) do table.insert(self.options, option)
element[k] = v -- fixme
end end
self.arguments = parser.arguments
self.commands = parser.commands
end end
function Parser:make_target(element) function Parser:make_target(element)
@@ -45,151 +36,297 @@ function Parser:make_target(element)
end end
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 function State:parse(args)
for i = 1, select('#', ...) do args = args or arg
argument = select(i, ...)
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 else
self:apply_options(element, argument) self._current = nil
end end
end end
self:make_target(element) local argument = self._arguments[self._next_arg_i]
if argument then
element.mincount, element.maxcount = utils.parse_boundaries(element.count) self:_open(argument)
element.minargs, element.maxargs = utils.parse_boundaries(element.args) self:_pass(argument, data)
else
table.insert(self.arguments, element) local command = self.context[data]
table.insert(self.elements, element) if command and command.type == "command" then
self._result[command.target] = {{}}
return element 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 end
function Parser:option(name, ...) function State:get_result()
local element = { self:_check()
name = name,
aliases = {},
count = "0-1",
args = 1,
type = "option"
}
self:add_alias(element, name) local result = {}
local argument local invocations
for i = 1, select('#', ...) do for _, element in ipairs(self._all_elements) do
argument = select(i, ...) invocations = self._result[element.target]
if type(argument) == "string" then if element.maxcount == 1 then
self:add_alias(element, argument) 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 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
end end
self:make_target(element) return result
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
end end
-- DRY? function State:_check()
self:_assert(not self._parser.must_command, "a command is required")
function Parser:flag(name, ...) local invocations
local element = { for _, element in ipairs(self._all_elements) do
name = name, invocations = self._result[element.target] or {}
aliases = {},
count = "0-1",
args = 0,
type = "option"
}
self:add_alias(element, name) if element.type == "argument" and #invocations == 0 then
invocations[1] = {}
end
local argument if #invocations > element.maxcount then
for i = 1, select('#', ...) do if element.no_overwrite then
argument = select(i, ...) 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:_assert(#invocations >= element.mincount, "option %s must be used at least %d times", element.name, element.mincount)
self:add_alias(element, argument)
else for _, passed in ipairs(invocations) do
self:apply_options(element, argument) 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
end end
self:make_target(element) return self
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
end end
function Parser:command(name, ...) local Aliased = {}
local element = {
name = name,
aliases = {},
count = "0-1",
args = 0,
type = "command"
}
local command = Parser() -- fixme function Aliased:set_name(name)
for k, v in pairs(element) do table.insert(self.aliases, name)
command[k] = v
if not self.name then
self.name = name
end end
end
self:add_alias(command, name) local Named = {}
local argument function Named:set_name(name)
for i = 1, select('#', ...) do self.name = name
argument = select(i, ...) end
self:add_alias(command, argument)
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) local Command = Parser:extends {
command.minargs, command.maxargs = utils.parse_boundaries(command.args) 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 return command
end end
function Parser:group(...)
--
end
function Parser:mutually_exclusive(...)
local group = {
elements = {...}
}
table.insert(self.groups, group)
return group
end
function Parser:error(fmt, ...) function Parser:error(fmt, ...)
local msg = fmt:format(...) local msg = fmt:format(...)
io.stderr:write("Error: " .. msg .. "\n") io.stderr:write("Error: " .. msg .. "\n")
@@ -200,6 +337,51 @@ function Parser:assert(assertion, ...)
return assertion or self:error(...) return assertion or self:error(...)
end 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) function Parser:parse(args)
args = args or arg args = args or arg

101
src/interface.lua Normal file
View File

@@ -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

39
src/interface_test.lua Normal file
View File

@@ -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 == "?")

View File

@@ -1,6 +1,8 @@
local class = require "30log" local class = require "30log"
local State = class() local State = class {
context = {}
}
function State:__init(parser) function State:__init(parser)
self.context = {} self.context = {}