Redesign argument storing

* Use state objects instead of tons of locals in the main
  function.
* Use actions for storing arguments into result table.
  Actions are now called at the end of each invocation,
  with result table, target index, arguments and overwrite flag as
  arguments.
* Remove command actions.
* Improve error messages, refer to options by the last used alias
  instead of the main name.

TODO:

* Improve error messages further ("argument 'foo' is required"
  -> "missing argument 'foo'", etc.).
* Add actions for positional arguments.
* Add actions for commands (should be called with final results
  after parsing is over, in "innermost first" order).
* Allow referring to built-in actions by strings a-la Python
  (e.g. action = "store_false").
* Allow setting initial value to be stored at target index
  for each option (perhaps use default value for that).
* Add more tests, particularly for actions.
This commit is contained in:
mpeterv
2015-10-29 14:20:58 +03:00
parent 98114c6976
commit 247c8a9cce
7 changed files with 426 additions and 388 deletions

View File

@@ -3,8 +3,14 @@ getmetatable(Parser()).error = function(_, msg) error(msg) end
describe("tests related to actions", function()
it("calls actions for options", function()
local action1 = spy.new(function() end)
local action2 = spy.new(function() end)
local action1 = spy.new(function(_, _, arg)
assert.equal("nowhere", arg)
end)
local expected_args = {"Alice", "Bob"}
local action2 = spy.new(function(_, _, args)
assert.same(expected_args, args)
expected_args = {"Emma", "John"}
end)
local parser = Parser()
parser:option "-f" "--from" {
@@ -16,8 +22,7 @@ describe("tests related to actions", function()
args = 2
}
local args = parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"}
assert.same({from = "nowhere", pair = {{"Alice", "Bob"}, {"Emma", "John"}}}, args)
parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"}
assert.spy(action1).called(1)
assert.spy(action2).called(2)
end)
@@ -39,27 +44,9 @@ describe("tests related to actions", function()
action = function(...) return action3(...) end
}
local args = parser:parse{"-vv", "--quiet"}
assert.same({verbose = 2, quiet = true}, args)
parser:parse{"-vv", "--quiet"}
assert.spy(action1).called(2)
assert.spy(action2).called(1)
assert.spy(action3).called(0)
end)
it("calls actions for commands", function()
local action = spy.new(function() end)
local parser = Parser "name"
parser:flag "-v" "--verbose" {
count = "0-3"
}
local add = parser:command "add" {
action = function(...) return action(...) end
}
add:argument "something"
local args = parser:parse{"add", "me"}
assert.same({add = true, verbose = 0, something = "me"}, args)
assert.spy(action).called(1)
end)
end)

View File

@@ -97,7 +97,7 @@ describe("tests related to positional arguments", function()
local parser = Parser()
parser:argument "foo"
assert.has_error(function() parser:parse{} end, "too few arguments")
assert.has_error(function() parser:parse{} end, "argument 'foo' is required")
end)
it("handles extra arguments with several arguments correctly", function()
@@ -113,7 +113,7 @@ describe("tests related to positional arguments", function()
parser:argument "foo1"
parser:argument "foo2"
assert.has_error(function() parser:parse{"bar"} end, "too few arguments")
assert.has_error(function() parser:parse{"bar"} end, "argument 'foo2' is required")
end)
it("handles too few arguments with multi-argument correctly", function()
@@ -121,7 +121,7 @@ describe("tests related to positional arguments", function()
parser:argument "foo" {
args = "+"
}
assert.has_error(function() parser:parse{} end, "too few arguments")
assert.has_error(function() parser:parse{} end, "argument 'foo' is required")
end)
it("handles too many arguments with multi-argument correctly", function()
@@ -137,7 +137,7 @@ describe("tests related to positional arguments", function()
parser:argument "foo" {
args = "2-4"
}
assert.has_error(function() parser:parse{"foo"} end, "too few arguments")
assert.has_error(function() parser:parse{"foo"} end, "argument 'foo' requires at least 2 arguments")
end)
it("handles too many arguments with several multi-arguments correctly", function()
@@ -159,7 +159,7 @@ describe("tests related to positional arguments", function()
parser:argument "foo2" {
args = "*"
}
assert.has_error(function() parser:parse{} end, "too few arguments")
assert.has_error(function() parser:parse{} end, "argument 'foo1' is required")
end)
end)
end)

View File

@@ -55,7 +55,7 @@ describe("tests related to default values", function()
assert.same({output = "a.out"}, args)
args = parser:parse{"--output", "foo.txt"}
assert.same({output = "foo.txt"}, args)
assert.has_error(function() parser:parse{"-o"} end, "too few arguments")
assert.has_error(function() parser:parse{"-o"} end, "option '-o' requires an argument")
end)
it("handles option with default value for multi-argument option correctly", function()

View File

@@ -20,7 +20,7 @@ describe("tests related to CLI behaviour #unsafe", function()
assert.equal([[
Usage: ]]..script..[[ [-v] [-h] <input> [<command>] ...
Error: too few arguments
Error: argument 'input' is required
]], get_output(""))
end)
@@ -62,7 +62,7 @@ Did you mean 'install'?
assert.equal([[
Usage: ]]..script..[[ install [-f <from>] [-h] <rock> [<version>]
Error: too few arguments
Error: argument 'rock' is required
]], get_output("foo install"))
end)

View File

@@ -258,7 +258,8 @@ describe("tests related to options", function()
it("handles lack of required argument correctly", function()
local parser = Parser()
parser:option "-s" "--server"
assert.has_error(function() parser:parse{"--server"} end, "too few arguments")
assert.has_error(function() parser:parse{"--server"} end, "option '--server' requires an argument")
assert.has_error(function() parser:parse{"-s"} end, "option '-s' requires an argument")
end)
it("handles unknown options correctly", function()
@@ -289,7 +290,7 @@ describe("tests related to options", function()
count = 1,
overwrite = false
}
assert.has_error(function() parser:parse{"-qq"} end, "option '-q' must be used at most 1 time")
assert.has_error(function() parser:parse{"-qq"} end, "option '-q' must be used 1 time")
end)
it("handles too few invocations correctly", function()

View File

@@ -15,13 +15,13 @@ describe("tests related to :pparse()", function()
parser:argument "foo"
local ok, errmsg = parser:pparse{}
assert.is_false(ok)
assert.equal("too few arguments", errmsg)
assert.equal("argument 'foo' is required", errmsg)
end)
it("still raises an error if it is caused by misconfiguration", function()
it("rethrows errors from callbacks", function()
local parser = Parser()
parser:flag "--foo"
:action(error)
assert.has_error(function() parser:pparse{"--foo"} end)
:action(function() error("some error message") end)
assert.error_matches(function() parser:pparse{"--foo"} end, "some error message")
end)
end)

View File

@@ -19,7 +19,6 @@
-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function deep_update(t1, t2)
for k, v in pairs(t2) do
if type(v) == "table" then
@@ -35,24 +34,25 @@ end
-- A property is a tuple {name, callback}.
-- properties.args is number of properties that can be set as arguments
-- when calling an object.
local function new_class(prototype, properties, parent)
local function class(prototype, properties, parent)
-- Class is the metatable of its instances.
local class = {}
class.__index = class
local cl = {}
cl.__index = cl
if parent then
class.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
else
class.__prototype = prototype
cl.__prototype = prototype
end
if properties then
local names = {}
-- Create setter methods and fill set of property names.
for _, property in ipairs(properties) do
local name, callback = property[1], property[2]
class[name] = function(self, value)
cl[name] = function(self, value)
if not callback(self, value) then
self["_" .. name] = value
end
@@ -63,7 +63,7 @@ local function new_class(prototype, properties, parent)
names[name] = true
end
function class.__call(self, ...)
function cl.__call(self, ...)
-- When calling an object, if the first argument is a table,
-- interpret keys as property names, else delegate arguments
-- to corresponding setters in order.
@@ -91,6 +91,7 @@ local function new_class(prototype, properties, parent)
return self
end
end
-- If indexing class fails, fallback to its parent.
local class_metatable = {}
@@ -104,7 +105,7 @@ local function new_class(prototype, properties, parent)
return object(...)
end
return setmetatable(class, class_metatable)
return setmetatable(cl, class_metatable)
end
local function typecheck(name, types, value)
@@ -204,7 +205,7 @@ local add_help = {"add_help", function(self, value)
end
end}
local Parser = new_class({
local Parser = class({
_arguments = {},
_options = {},
_commands = {},
@@ -223,7 +224,7 @@ local Parser = new_class({
add_help
})
local Command = new_class({
local Command = class({
_aliases = {}
}, {
args = 3,
@@ -235,11 +236,10 @@ local Command = new_class({
typechecked("help", "string"),
typechecked("require_command", "boolean"),
typechecked("handle_options", "boolean"),
typechecked("action", "function"),
add_help
}, Parser)
local Argument = new_class({
local Argument = class({
_minargs = 1,
_maxargs = 1,
_mincount = 1,
@@ -259,7 +259,7 @@ local Argument = new_class({
typechecked("argname", "string", "table")
})
local Option = new_class({
local Option = class({
_aliases = {},
_mincount = 0,
_overwrite = true
@@ -322,22 +322,42 @@ function Argument:_get_usage()
return usage
end
function Argument:_get_type()
local actions = {}
function actions.store_true(result, target)
result[target] = true
end
function actions.store(result, target, argument)
result[target] = argument
end
function actions.count(result, target, _, overwrite)
if not overwrite then
result[target] = result[target] + 1
end
end
function actions.append(result, target, argument, overwrite)
table.insert(result[target], argument)
if overwrite then
table.remove(result[target], 1)
end
end
function Argument:_get_action()
if self._maxcount == 1 then
if self._maxargs == 0 then
return "flag"
elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then
return "arg"
return self._action or actions.store_true, nil
else
return "multiarg"
return self._action or actions.store, nil
end
else
if self._maxargs == 0 then
return "counter"
elseif self._maxargs == 1 and self._minargs == 1 then
return "multicount"
return self._action or actions.count, 0
else
return "twodimensional"
return self._action or actions.append, {}
end
end
end
@@ -411,6 +431,10 @@ function Option:_get_usage()
return usage
end
function Argument:_get_default_target()
return self._name
end
function Option:_get_default_target()
local res
@@ -635,8 +659,9 @@ local function get_tip(context, wrong_name)
local possible_names = {}
for name in pairs(context) do
for i=1, #name do
possible_name = name:sub(1, i-1) .. name:sub(i+1)
if type(name) == "string" then
for i = 1, #name do
possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
if not context_pool[possible_name] then
context_pool[possible_name] = {}
@@ -645,9 +670,10 @@ local function get_tip(context, wrong_name)
table.insert(context_pool[possible_name], name)
end
end
end
for i=1, #wrong_name+1 do
possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1)
for i = 1, #wrong_name + 1 do
possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1)
if context[possible_name] then
possible_names[possible_name] = true
@@ -659,6 +685,7 @@ local function get_tip(context, wrong_name)
end
local first = next(possible_names)
if first then
if next(possible_names, first) then
local possible_names_arr = {}
@@ -677,294 +704,345 @@ local function get_tip(context, wrong_name)
end
end
local function plural(x)
if x == 1 then
return ""
end
local ElementState = class({
invocations = 0
})
return "s"
function ElementState:__call(state, element)
self.state = state
self.result = state.result
self.element = element
self.target = element._target or element:_get_default_target()
self.action, self.result[self.target] = element:_get_action()
return self
end
-- Compatibility with strict.lua and other checkers:
local default_cmdline = rawget(_G, "arg") or {}
function ElementState:error(fmt, ...)
self.state:error(fmt, ...)
end
function Parser:_parse(args, errhandler)
args = args or default_cmdline
local parser
local charset
local options = {}
local arguments = {}
local commands
local option_mutexes = {}
local used_mutexes = {}
local opt_context = {}
local com_context
local result = {}
local invocations = {}
local passed = {}
local cur_option
local cur_arg_i = 1
local cur_arg
local targets = {}
local handle_options = true
function ElementState:convert(argument)
local converter = self.element._convert
local function error_(fmt, ...)
return errhandler(parser, fmt:format(...))
end
local function assert_(assertion, ...)
return assertion or error_(...)
end
local function convert(element, data)
if element._convert then
if converter then
local ok, err
if type(element._convert) == "function" then
ok, err = element._convert(data)
if type(converter) == "function" then
ok, err = converter(argument)
else
ok = element._convert[data]
ok = converter[argument]
end
assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'")
data = ok
if ok == nil then
self:error(err and "%s" or "malformed argument '%s'", err or argument)
end
return data
argument = ok
end
local invoke, pass, close
return argument
end
function invoke(element)
local overwrite = false
function ElementState:default(mode)
return self.element._defmode:find(mode) and self.element._default
end
if invocations[element] == element._maxcount then
if element._overwrite then
overwrite = true
local function bound(noun, min, max, is_max)
local res = ""
if min ~= max then
res = "at " .. (is_max and "most" or "least") .. " "
end
local number = is_max and max or min
return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s")
end
function ElementState:invoke(alias)
self.open = true
self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name)
self.overwrite = false
if self.invocations >= self.element._maxcount then
if self.element._overwrite then
self.overwrite = true
else
error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount))
self:error("%s must be used %s", self.name, bound("time", self.element._mincount, self.element._maxcount, true))
end
else
invocations[element] = invocations[element]+1
self.invocations = self.invocations + 1
end
passed[element] = 0
local type_ = element:_get_type()
local target = targets[element]
self.args = {}
if type_ == "flag" then
result[target] = true
elseif type_ == "multiarg" then
result[target] = {}
elseif type_ == "counter" then
if not overwrite then
result[target] = result[target]+1
end
elseif type_ == "multicount" then
if overwrite then
table.remove(result[target], 1)
end
elseif type_ == "twodimensional" then
table.insert(result[target], {})
if overwrite then
table.remove(result[target], 1)
end
if self.element._maxargs <= 0 then
self:close()
end
if element._maxargs == 0 then
close(element)
end
return self.open
end
function ElementState:pass(argument)
argument = self:convert(argument)
table.insert(self.args, argument)
if #self.args >= self.element._maxargs then
self:close()
end
function pass(element, data)
passed[element] = passed[element]+1
data = convert(element, data)
local type_ = element:_get_type()
local target = targets[element]
return self.open
end
if type_ == "arg" then
result[target] = data
elseif type_ == "multiarg" or type_ == "multicount" then
table.insert(result[target], data)
elseif type_ == "twodimensional" then
table.insert(result[target][#result[target]], data)
function ElementState:complete_invocation()
while #self.args < self.element._minargs do
self:pass(self.element._default)
end
end
if passed[element] == element._maxargs then
close(element)
end
end
function ElementState:close()
if self.open then
self.open = false
local function complete_invocation(element)
while passed[element] < element._minargs do
pass(element, element._default)
end
end
function close(element)
if passed[element] < element._minargs then
if element._default and element._defmode:find "a" then
complete_invocation(element)
if #self.args < self.element._minargs then
if self:default("a") then
self:complete_invocation()
else
error_("too few arguments")
end
else
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
if #self.args == 0 then
if getmetatable(self.element) == Argument then
self:error("%s is required", self.name)
elseif self.element._maxargs == 1 then
self:error("%s requires an argument", self.name)
end
end
local function switch(p)
parser = p
self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs))
end
end
local args = self.args
if self.element._maxargs <= 1 then
args = args[1]
end
if self.element._maxargs == 1 and self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then
args = self.args
end
self.action(self.result, self.target, args, self.overwrite)
end
end
local ParseState = class({
result = {},
options = {},
arguments = {},
argument_i = 1,
element_to_mutexes = {},
mutex_to_used_option = {}
})
function ParseState:__call(parser, error_handler)
self.parser = parser
self.error_handler = error_handler
self.charset = parser:_update_charset()
self:switch(parser)
return self
end
function ParseState:error(fmt, ...)
self.error_handler(self.parser, fmt:format(...))
end
function ParseState:switch(parser)
self.parser = parser
for _, option in ipairs(parser._options) do
table.insert(options, option)
option = ElementState(self, option)
table.insert(self.options, option)
for _, alias in ipairs(option._aliases) do
opt_context[alias] = option
for _, alias in ipairs(option.element._aliases) do
self.options[alias] = option
end
local type_ = option:_get_type()
targets[option] = option._target or option:_get_default_target()
if type_ == "counter" then
result[targets[option]] = 0
elseif type_ == "multicount" or type_ == "twodimensional" then
result[targets[option]] = {}
end
invocations[option] = 0
end
for _, mutex in ipairs(parser._mutexes) do
for _, option in ipairs(mutex) do
if not option_mutexes[option] then
option_mutexes[option] = {mutex}
else
table.insert(option_mutexes[option], mutex)
if not self.element_to_mutexes[option] then
self.element_to_mutexes[option] = {}
end
table.insert(self.element_to_mutexes[option], mutex)
end
end
for _, argument in ipairs(parser._arguments) do
table.insert(arguments, argument)
invocations[argument] = 0
targets[argument] = argument._target or argument._name
invoke(argument)
argument = ElementState(self, argument)
table.insert(self.arguments, argument)
argument:invoke()
end
handle_options = parser._handle_options
cur_arg = arguments[cur_arg_i]
commands = parser._commands
com_context = {}
for _, command in ipairs(commands) do
targets[command] = command._target or command._name
self.handle_options = parser._handle_options
self.argument = self.arguments[self.argument_i]
self.commands = parser._commands
for _, command in ipairs(self.commands) do
for _, alias in ipairs(command._aliases) do
com_context[alias] = command
end
self.commands[alias] = command
end
end
end
local function get_option(name)
return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name))
end
function ParseState:get_option(name)
local option = self.options[name]
local function do_action(element)
if element._action then
element._action()
end
end
local function handle_argument(data)
if cur_option then
pass(cur_option, data)
elseif cur_arg then
pass(cur_arg, data)
if not option then
self:error("unknown option '%s'%s", name, get_tip(self.options, name))
else
local com = com_context[data]
return option
end
end
if not com then
if #commands > 0 then
error_("unknown command '%s'%s", data, get_tip(com_context, data))
function ParseState:get_command(name)
local command = self.commands[name]
if not command then
if #self.commands > 0 then
self:error("unknown command '%s'%s", name, get_tip(self.commands, name))
else
error_("too many arguments")
self:error("too many arguments")
end
else
result[targets[com]] = true
do_action(com)
switch(com)
end
end
return command
end
end
local function handle_option(data)
if cur_option then
close(cur_option)
end
function ParseState:invoke(option, name)
self:close()
cur_option = opt_context[data]
if self.element_to_mutexes[option.element] then
for _, mutex in ipairs(self.element_to_mutexes[option.element]) do
local used_option = self.mutex_to_used_option[mutex]
if option_mutexes[cur_option] then
for _, mutex in ipairs(option_mutexes[cur_option]) do
if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then
error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name)
if used_option and used_option ~= option then
self:error("option '%s' can not be used together with %s", name, used_option.name)
else
used_mutexes[mutex] = cur_option
self.mutex_to_used_option[mutex] = option
end
end
end
do_action(cur_option)
invoke(cur_option)
if option:invoke(name) then
self.option = option
end
end
function ParseState:pass(arg)
if self.option then
if not self.option:pass(arg) then
self.option = nil
end
elseif self.argument then
if not self.argument:pass(arg) then
self.argument_i = self.argument_i + 1
self.argument = self.arguments[self.argument_i]
end
else
local command = self:get_command(arg)
self.result[command._target or command._name] = true
self:switch(command)
end
end
function ParseState:close()
if self.option then
self.option:close()
self.option = nil
end
end
function ParseState:finalize()
self:close()
for i = self.argument_i, #self.arguments do
local argument = self.arguments[i]
if #argument.args == 0 and argument:default("u") then
argument:complete_invocation()
else
argument:close()
end
end
local function mainloop()
if self.parser._require_command and #self.commands > 0 then
self:error("a command is required")
end
for _, data in ipairs(args) do
for _, option in ipairs(self.options) do
local name = option.name or ("option '%s'"):format(option.element._name)
if option.invocations == 0 then
if option:default("u") then
option:invoke(name)
option:complete_invocation()
option:close()
end
end
local mincount = option.element._mincount
if option.invocations < mincount then
if option:default("a") then
while option.invocations < mincount do
option:invoke(name)
option:close()
end
else
self:error("%s must be used %s", name, bound("time", mincount, option.element._maxcount))
end
end
end
end
function ParseState:parse(args)
for _, arg in ipairs(args) do
local plain = true
local first, name, option
if handle_options then
first = data:sub(1, 1)
if charset[first] then
if #data > 1 then
if self.handle_options then
local first = arg:sub(1, 1)
if self.charset[first] then
if #arg > 1 then
plain = false
if data:sub(2, 2) == first then
if #data == 2 then
if cur_option then
close(cur_option)
if arg:sub(2, 2) == first then
if #arg == 2 then
self:close()
self.handle_options = false
else
local equals = arg:find "="
if equals then
local name = arg:sub(1, equals - 1)
local option = self:get_option(name)
if option.element._maxargs <= 0 then
self:error("option '%s' does not take arguments", name)
end
handle_options = false
self:invoke(option, name)
self:pass(arg:sub(equals + 1))
else
local equal = data:find "="
if equal then
name = data:sub(1, equal-1)
option = get_option(name)
assert_(option._maxargs > 0, "option '%s' does not take arguments", name)
handle_option(data:sub(1, equal-1))
handle_argument(data:sub(equal+1))
else
get_option(data)
handle_option(data)
local option = self:get_option(arg)
self:invoke(option, arg)
end
end
else
for i = 2, #data do
name = first .. data:sub(i, i)
option = get_option(name)
handle_option(name)
for i = 2, #arg do
local name = first .. arg:sub(i, i)
local option = self:get_option(name)
self:invoke(option, name)
if i ~= #data and option._minargs > 0 then
handle_argument(data:sub(i+1))
if i ~= #arg and option.element._minargs > 0 and option.element._maxargs > 0 then
self:pass(arg:sub(i + 1))
break
end
end
@@ -974,53 +1052,12 @@ function Parser:_parse(args, errhandler)
end
if plain then
handle_argument(data)
end
end
end
switch(self)
charset = parser:_update_charset()
mainloop()
if cur_option then
close(cur_option)
end
while cur_arg do
if passed[cur_arg] == 0 and cur_arg._default and cur_arg._defmode:find "u" then
complete_invocation(cur_arg)
else
close(cur_arg)
self:pass(arg)
end
end
if parser._require_command and #commands > 0 then
error_("a command is required")
end
for _, option in ipairs(options) do
if invocations[option] == 0 then
if option._default and option._defmode:find "u" then
invoke(option)
complete_invocation(option)
close(option)
end
end
if invocations[option] < option._mincount then
if option._default and option._defmode:find "a" then
while invocations[option] < option._mincount do
invoke(option)
close(option)
end
else
error_("option '%s' must be used at least %d time%s", option._name, option._mincount, plural(option._mincount))
end
end
end
return result
self:finalize()
return self.result
end
function Parser:error(msg)
@@ -1028,24 +1065,37 @@ function Parser:error(msg)
os.exit(1)
end
-- Compatibility with strict.lua and other checkers:
local default_cmdline = rawget(_G, "arg") or {}
function Parser:_parse(args, error_handler)
return ParseState(self, error_handler):parse(args or default_cmdline)
end
function Parser:parse(args)
return self:_parse(args, Parser.error)
return self:_parse(args, self.error)
end
local function xpcall_error_handler(err)
return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2)
end
function Parser:pparse(args)
local errmsg
local ok, result = pcall(function()
local parse_error
local ok, result = xpcall(function()
return self:_parse(args, function(_, err)
errmsg = err
return error()
end)
parse_error = err
error(err, 0)
end)
end, xpcall_error_handler)
if ok then
return true, result
elseif not parse_error then
error(result, 0)
else
assert(errmsg, result)
return false, errmsg
return false, parse_error
end
end