Finished working on new interface and stuff

This commit is contained in:
mpeterv
2014-01-18 13:12:56 +04:00
parent dafff3d63e
commit d492dc5e0f
9 changed files with 264 additions and 690 deletions

View File

@@ -16,8 +16,6 @@ dependencies = {
build = {
type = "builtin",
modules = {
argparse = "src/argparse.lua",
["argparse.state"] = "src/state.lua",
["argparse.utils"] = "src/utils.lua"
argparse = "src/argparse.lua"
}
}

View File

@@ -77,16 +77,6 @@ describe("tests related to positional arguments", function()
end)
describe("passing incorrect arguments", function()
local old_parser = argparse.parser
setup(function()
argparse.parser = old_parser:extends()
function argparse.parser:error(fmt, ...)
error(fmt:format(...))
end
end)
it("handles extra arguments with empty parser correctly", function()
local parser = argparse.parser()
@@ -132,17 +122,17 @@ describe("tests related to positional arguments", function()
it("handles too few arguments with multi-argument correctly", function()
local parser = argparse.parser()
parser:argument("foo", {
parser:argument "foo" {
args = "+"
})
}
assert.has_error(curry(parser.parse, parser, {}), "too few arguments")
end)
it("handles too many arguments with multi-argument correctly", function()
local parser = argparse.parser()
parser:argument("foo", {
parser:argument "foo" {
args = "2-4"
})
}
assert.has_error(curry(parser.parse, parser, {"foo", "bar", "baz", "qu", "quu"}), "too many arguments")
end)

View File

@@ -151,16 +151,6 @@ describe("tests related to options", function()
assert.same(args, {exclude = {{"Alice", "Bob"}, {"Emma", "Jacob"}}})
end)
it("handles multi-count option with optional argument correctly", function()
local parser = argparse.parser()
parser:option("-w", "--why", "--why-would-someone-use-this", {
count = "*",
args = "?"
})
local args = parser:parse({"-w", "-wfoo", "--why=because", "-ww"})
assert.same(args, {why = {{}, {"foo"}, {"because"}, {}, {}}})
end)
it("handles multi-count flag correctly", function()
local parser = argparse.parser()
parser:flag("-q", "--quiet", {
@@ -191,15 +181,6 @@ describe("tests related to options", function()
end)
describe("passing incorrect options", function()
local old_parser = argparse.parser
setup(function()
argparse.parser = old_parser:extends()
function argparse.parser:error(fmt, ...)
error(fmt:format(...))
end
end)
it("handles lack of required argument correctly", function()
local parser = argparse.parser()
parser:option("-s", "--server")
@@ -222,9 +203,17 @@ describe("tests related to options", function()
local parser = argparse.parser()
parser:flag("-q", "--quiet", {
count = 1,
no_overwrite = true
overwrite = false
})
assert.has_error(curry(parser.parse, parser, {"-qq"}), "option -q must be used at most 1 times")
end)
it("handles too few invocations correctly", function()
local parser = argparse.parser()
parser:option("-f", "--foo", {
count = "3-4"
})
assert.has_error(curry(parser.parse, parser, {"-fFOO", "-fBAR"}), "option -f must be used at least 3 times")
end)
end)
end)

View File

@@ -1,39 +0,0 @@
local utils = require "argparse.utils"
describe("tests related to utils.parse_boundaries", function()
it("handles * correctly", function()
local min, max = utils.parse_boundaries("*")
assert.equal(min, 0)
assert.equal(max, math.huge)
end)
it("handles + correctly", function()
local min, max = utils.parse_boundaries("+")
assert.equal(min, 1)
assert.equal(max, math.huge)
end)
it("handles ? correctly", function()
local min, max = utils.parse_boundaries("?")
assert.equal(min, 0)
assert.equal(max, 1)
end)
it("handles numbers correctly", function()
local min, max = utils.parse_boundaries(42)
assert.equal(min, 42)
assert.equal(max, 42)
end)
it("handles numbers+ correctly", function()
local min, max = utils.parse_boundaries("42+")
assert.equal(min, 42)
assert.equal(max, math.huge)
end)
it("handles ranges correctly", function()
local min, max = utils.parse_boundaries("42-96")
assert.equal(min, 42)
assert.equal(max, 96)
end)
end)

View File

@@ -2,203 +2,10 @@ local argparse = {}
local class = require "30log"
function State:invoke(element)
if not self.invocatons then
function State:push(option)
if self.top_is_opt then
self:pop()
end
self:invoke(option)
if option.maxargs ~= 0 then
table.insert(self.stack, option)
self.top_is_opt = true
end
end
function State:pop()
if self.top_is_opt
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:_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(...)
self(...)
end
function Declarative:__call(...)
@@ -228,6 +35,7 @@ function Declarative:__call(...)
end
local Parser = class {
__name = "Parser",
arguments = {},
options = {},
commands = {},
@@ -235,55 +43,73 @@ local Parser = class {
}:include(Declarative)
local Command = Parser:extends {
__name = "Command",
aliases = {}
}:include(Declarative)
local Argument = class {
__name = "Argument",
args = 1,
count = 1,
fields = {"name", "description", "target", "args", "default", "convert"}
fields = {
"name", "description", "target", "args",
"minargs", "maxargs", "default", "convert"
}
}:include(Declarative)
local Option = class {
__name = "Option",
aliases = {},
args = 1,
count = "?",
fields = {"name", "aliases", "description", "target", "args", "count", "default", "convert"}
overwrite = true,
fields = {
"name", "aliases", "description", "target",
"args", "minargs", "maxargs", "count",
"mincount", "maxcount", "default", "convert",
"overwrite"
}
}:include(Declarative)
local Flag = Option:extends {
__name = "Flag",
args = 0
}:include(Declarative)
function Parser:argument(...)
local argument = Argument(...)
local argument = Argument:new(...)
table.insert(self.arguments, argument)
return argument
end
function Parser:option(...)
local option = Option(...)
local option = Option:new(...)
table.insert(self.options, option)
return option
end
function Parser:flag(...)
local flag = Flag(...)
local flag = Flag:new(...)
table.insert(self.options, flag)
return flag
end
function Parser:command(...)
local command = Command(...)
local command = Command:new(...)
table.insert(self.commands, command)
return command
end
function Parser:error(fmt, ...)
local msg = fmt:format(...)
io.stderr:write("Error: " .. msg .. "\n")
if _TEST then
error(msg)
else
io.stderr:write("Error: " .. msg .. "\r\n")
os.exit(1)
end
end
function Parser:assert(assertion, ...)
return assertion or self:error(...)
@@ -291,7 +117,7 @@ end
function Parser:make_charset()
if not self.charset then
self.charset = {}
self.charset = {["-"] = true}
for _, command in ipairs(self.commands) do
command:make_charset()
@@ -332,7 +158,49 @@ function Parser:make_targets()
end
end
function self:make_command_names()
local function 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
function Parser:make_boundaries()
for _, elements in ipairs{self.arguments, self.options} do
for _, element in ipairs(elements) do
if not element.minargs or not element.maxargs then
element.minargs, element.maxargs = parse_boundaries(element.args)
end
if not element.mincount or not element.maxcount then
element.mincount, element.maxcount = parse_boundaries(element.count)
end
end
end
end
function Parser:make_command_names()
for _, command in ipairs(self.commands) do
command.name = self.name .. " " .. command.name
end
@@ -341,6 +209,7 @@ end
function Parser:prepare()
self:make_charset()
self:make_targets()
self:make_boundaries()
self:make_command_names()
return self
end
@@ -356,6 +225,57 @@ function Parser:parse(args)
local opt_context = {}
local com_context
local result = {}
local cur_option, cur_arg_i, cur_arg
local function close(element)
local invocations = result[element.target]
local passed = invocations[#invocations]
if #passed < element.minargs then
if element.default then
while #passed < element.minargs do
table.insert(passed, element.default)
end
else
parser:error("too few arguments")
end
end
if element == cur_option then
cur_option = nil
elseif element == cur_arg then
cur_arg_i = cur_arg_i+1
cur_arg = arguments[cur_arg_i]
end
end
local function invoke(element)
local invocations = result[element.target]
if #invocations == element.maxcount then
if element.overwrite then
table.remove(invocations, 1)
else
parser:error("option %s must be used at most %d times", element.name, element.maxcount)
end
end
table.insert(result[element.target], {})
if element.maxargs == 0 then
close(element)
end
end
local function pass(element, data)
local invocations = result[element.target]
local passed = invocations[#invocations]
table.insert(passed, data)
if #passed == element.maxargs then
close(element)
end
end
local function switch(p)
parser = p:prepare()
@@ -367,9 +287,19 @@ function Parser:parse(args)
for _, alias in ipairs(option.aliases) do
opt_context[alias] = option
end
result[option.target] = {}
end
arguments = p.arguments
cur_arg_i = 1
cur_arg = arguments[cur_arg_i]
for _, argument in ipairs(arguments) do
result[argument.target] = {}
invoke(argument)
end
commands = p.commands
com_context = {}
@@ -381,13 +311,36 @@ function Parser:parse(args)
end
local function handle_argument(data)
if cur_option then
pass(cur_option, data)
elseif cur_arg then
pass(cur_arg, data)
else
local com = com_context[data]
if not com then
if #commands > 0 then
parser:error("wrong command") -- add lev-based guessing here
else
parser:error("too many arguments")
end
else
result[com.target] = true
switch(com)
end
end
end
local function handle_option(data)
if cur_option then
close(cur_option)
end
cur_option = opt_context[data]
invoke(cur_option)
end
local function mainloop()
local handle_options = true
for _, data in ipairs(args) do
@@ -396,17 +349,17 @@ function Parser:parse(args)
if handle_options then
first = data:sub(1, 1)
if self.charset[first] then
if charset[first] then
if #data > 1 then
if data:sub(2, 2):match "[a-zA-Z]" then
plain = false
for i = 2, #data do
name = first .. data:sub(i, i)
option = self:assert(self.opt_context[name], "unknown option " .. name)
option = parser:assert(opt_context[name], "unknown option " .. name)
handle_option(name)
if i ~= #data and not (options.minargs == 0 and self.opt_context[first .. data:sub(i+1, i+1)]) then
if i ~= #data and option.minargs > 0 then
handle_argument(data:sub(i+1))
break
end
@@ -421,8 +374,8 @@ function Parser:parse(args)
local equal = data:find "="
if equal then
name = data:sub(1, equal-1)
option = self:assert(self.opt_context[name], "unknown option " .. name)
self:assert(option.maxargs > 0, "option " .. name .. " doesn't take arguments")
option = parser:assert(opt_context[name], "unknown option " .. name)
parser:assert(option.maxargs > 0, "option " .. name .. " doesn't take arguments")
handle_option(data:sub(1, equal-1))
handle_argument(data:sub(equal+1))
@@ -439,6 +392,62 @@ function Parser:parse(args)
handle_argument(data)
end
end
end
local function format()
local new_result = {}
local invocations
for _, elements in ipairs{options, arguments} do
for _, element in ipairs(elements) do
invocations = result[element.target]
parser:assert(#invocations >= element.mincount,
"option %s must be used at least %d times", element.name, element.mincount)
if element.maxcount == 1 then
if element.maxargs == 0 then
if #invocations > 0 then
new_result[element.target] = true
end
elseif element.maxargs == 1 and element.minargs == 1 then
if #invocations > 0 then
new_result[element.target] = invocations[1][1]
end
else
new_result[element.target] = invocations[1]
end
else
if element.maxargs == 0 then
new_result[element.target] = #invocations
elseif element.maxargs == 1 and element.minargs == 1 then
new_result[element.target] = {}
for _, passed in ipairs(invocations) do
table.insert(new_result[element.target], passed[1])
end
else
new_result[element.target] = invocations
end
end
end
end
result = new_result
end
switch(self)
mainloop()
if cur_option then
close(cur_option)
end
while cur_arg do
close(cur_arg)
end
format()
return result
end

View File

@@ -1,101 +0,0 @@
-- 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

View File

@@ -1,39 +0,0 @@
--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,202 +0,0 @@
local class = require "30log"
local State = class {
context = {}
}
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 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
return State

View File

@@ -1,31 +0,0 @@
local utils = {}
function utils.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
return utils