mirror of
https://github.com/TangentFoxy/argparse.git
synced 2025-07-28 02:52:20 +00:00
declarative interface
This commit is contained in:
492
src/argparse.lua
492
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
|
||||
function State:switch(parser)
|
||||
self.parser = parser
|
||||
self.parser:make_targets()
|
||||
|
||||
for _, option in ipairs(parser.options) do
|
||||
table.insert(self.options, option)
|
||||
end
|
||||
|
||||
function Parser:apply_options(element, options)
|
||||
for k, v in pairs(options) do
|
||||
element[k] = v -- fixme
|
||||
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 = {},
|
||||
|
||||
|
||||
function State:parse(args)
|
||||
args = args or arg
|
||||
|
||||
|
||||
|
||||
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 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
|
||||
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 element.type == "argument" and #invocations == 0 then
|
||||
invocations[1] = {}
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 = {},
|
||||
charset = {"-"},
|
||||
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,
|
||||
type = "argument"
|
||||
}
|
||||
count = "?",
|
||||
fields = {"name", "aliases", "description", "target", "args", "count", "default", "convert"}
|
||||
}:include(Declarative):include(Aliased)
|
||||
|
||||
self:add_alias(element, name)
|
||||
local Flag = Option:extends {
|
||||
args = 0
|
||||
}:include(Declarative):include(Aliased)
|
||||
|
||||
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
|
||||
function Parser:argument(...)
|
||||
local argument = Argument(...)
|
||||
table.insert(self.arguments, argument)
|
||||
return argument
|
||||
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
|
||||
function Parser:option(...)
|
||||
local option = Option(...)
|
||||
table.insert(self.options, option)
|
||||
return option
|
||||
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
|
||||
function Parser:flag(...)
|
||||
local flag = Flag(...)
|
||||
table.insert(self.options, flag)
|
||||
return flag
|
||||
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
|
||||
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 = utils.parse_boundaries(element.count)
|
||||
element.minargs, element.maxargs = utils.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 = utils.parse_boundaries(command.count)
|
||||
command.minargs, command.maxargs = utils.parse_boundaries(command.args)
|
||||
|
||||
table.insert(self.elements, command)
|
||||
|
||||
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
|
||||
|
101
src/interface.lua
Normal file
101
src/interface.lua
Normal 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
39
src/interface_test.lua
Normal 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 == "?")
|
@@ -1,6 +1,8 @@
|
||||
local class = require "30log"
|
||||
|
||||
local State = class()
|
||||
local State = class {
|
||||
context = {}
|
||||
}
|
||||
|
||||
function State:__init(parser)
|
||||
self.context = {}
|
||||
|
Reference in New Issue
Block a user