mirror of
https://github.com/TangentFoxy/argparse.git
synced 2025-07-28 11:02:20 +00:00
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:
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
612
src/argparse.lua
612
src/argparse.lua
@@ -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,6 +659,7 @@ local function get_tip(context, wrong_name)
|
||||
local possible_names = {}
|
||||
|
||||
for name in pairs(context) do
|
||||
if type(name) == "string" then
|
||||
for i = 1, #name do
|
||||
possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
|
||||
|
||||
@@ -645,6 +670,7 @@ 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)
|
||||
@@ -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 ""
|
||||
local ElementState = class({
|
||||
invocations = 0
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
return "s"
|
||||
function ElementState:error(fmt, ...)
|
||||
self.state:error(fmt, ...)
|
||||
end
|
||||
|
||||
-- Compatibility with strict.lua and other checkers:
|
||||
local default_cmdline = rawget(_G, "arg") or {}
|
||||
function ElementState:convert(argument)
|
||||
local converter = self.element._convert
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
if self.element._maxargs <= 0 then
|
||||
self:close()
|
||||
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)
|
||||
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
|
||||
|
||||
return self.open
|
||||
end
|
||||
|
||||
function ElementState:complete_invocation()
|
||||
while #self.args < self.element._minargs do
|
||||
self:pass(self.element._default)
|
||||
end
|
||||
end
|
||||
|
||||
if element._maxargs == 0 then
|
||||
close(element)
|
||||
end
|
||||
end
|
||||
function ElementState:close()
|
||||
if self.open then
|
||||
self.open = false
|
||||
|
||||
function pass(element, data)
|
||||
passed[element] = passed[element]+1
|
||||
data = convert(element, data)
|
||||
local type_ = element:_get_type()
|
||||
local target = targets[element]
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
if passed[element] == element._maxargs then
|
||||
close(element)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
local function mainloop()
|
||||
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
|
||||
|
||||
for _, data in ipairs(args) do
|
||||
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
|
||||
|
||||
if self.parser._require_command and #self.commands > 0 then
|
||||
self:error("a command is required")
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
Reference in New Issue
Block a user