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()
|
describe("tests related to actions", function()
|
||||||
it("calls actions for options", function()
|
it("calls actions for options", function()
|
||||||
local action1 = spy.new(function() end)
|
local action1 = spy.new(function(_, _, arg)
|
||||||
local action2 = spy.new(function() end)
|
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()
|
local parser = Parser()
|
||||||
parser:option "-f" "--from" {
|
parser:option "-f" "--from" {
|
||||||
@@ -16,8 +22,7 @@ describe("tests related to actions", function()
|
|||||||
args = 2
|
args = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
local args = parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"}
|
parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"}
|
||||||
assert.same({from = "nowhere", pair = {{"Alice", "Bob"}, {"Emma", "John"}}}, args)
|
|
||||||
assert.spy(action1).called(1)
|
assert.spy(action1).called(1)
|
||||||
assert.spy(action2).called(2)
|
assert.spy(action2).called(2)
|
||||||
end)
|
end)
|
||||||
@@ -39,27 +44,9 @@ describe("tests related to actions", function()
|
|||||||
action = function(...) return action3(...) end
|
action = function(...) return action3(...) end
|
||||||
}
|
}
|
||||||
|
|
||||||
local args = parser:parse{"-vv", "--quiet"}
|
parser:parse{"-vv", "--quiet"}
|
||||||
assert.same({verbose = 2, quiet = true}, args)
|
|
||||||
assert.spy(action1).called(2)
|
assert.spy(action1).called(2)
|
||||||
assert.spy(action2).called(1)
|
assert.spy(action2).called(1)
|
||||||
assert.spy(action3).called(0)
|
assert.spy(action3).called(0)
|
||||||
end)
|
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)
|
end)
|
||||||
|
@@ -97,7 +97,7 @@ describe("tests related to positional arguments", function()
|
|||||||
local parser = Parser()
|
local parser = Parser()
|
||||||
parser:argument "foo"
|
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)
|
end)
|
||||||
|
|
||||||
it("handles extra arguments with several arguments correctly", function()
|
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 "foo1"
|
||||||
parser:argument "foo2"
|
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)
|
end)
|
||||||
|
|
||||||
it("handles too few arguments with multi-argument correctly", function()
|
it("handles too few arguments with multi-argument correctly", function()
|
||||||
@@ -121,7 +121,7 @@ describe("tests related to positional arguments", function()
|
|||||||
parser:argument "foo" {
|
parser:argument "foo" {
|
||||||
args = "+"
|
args = "+"
|
||||||
}
|
}
|
||||||
assert.has_error(function() parser:parse{} end, "too few arguments")
|
assert.has_error(function() parser:parse{} end, "argument 'foo' is required")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("handles too many arguments with multi-argument correctly", function()
|
it("handles too many arguments with multi-argument correctly", function()
|
||||||
@@ -137,7 +137,7 @@ describe("tests related to positional arguments", function()
|
|||||||
parser:argument "foo" {
|
parser:argument "foo" {
|
||||||
args = "2-4"
|
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)
|
end)
|
||||||
|
|
||||||
it("handles too many arguments with several multi-arguments correctly", function()
|
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" {
|
parser:argument "foo2" {
|
||||||
args = "*"
|
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)
|
end)
|
||||||
end)
|
end)
|
||||||
|
@@ -55,7 +55,7 @@ describe("tests related to default values", function()
|
|||||||
assert.same({output = "a.out"}, args)
|
assert.same({output = "a.out"}, args)
|
||||||
args = parser:parse{"--output", "foo.txt"}
|
args = parser:parse{"--output", "foo.txt"}
|
||||||
assert.same({output = "foo.txt"}, args)
|
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)
|
end)
|
||||||
|
|
||||||
it("handles option with default value for multi-argument option correctly", function()
|
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([[
|
assert.equal([[
|
||||||
Usage: ]]..script..[[ [-v] [-h] <input> [<command>] ...
|
Usage: ]]..script..[[ [-v] [-h] <input> [<command>] ...
|
||||||
|
|
||||||
Error: too few arguments
|
Error: argument 'input' is required
|
||||||
]], get_output(""))
|
]], get_output(""))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Did you mean 'install'?
|
|||||||
assert.equal([[
|
assert.equal([[
|
||||||
Usage: ]]..script..[[ install [-f <from>] [-h] <rock> [<version>]
|
Usage: ]]..script..[[ install [-f <from>] [-h] <rock> [<version>]
|
||||||
|
|
||||||
Error: too few arguments
|
Error: argument 'rock' is required
|
||||||
]], get_output("foo install"))
|
]], get_output("foo install"))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@@ -258,7 +258,8 @@ describe("tests related to options", function()
|
|||||||
it("handles lack of required argument correctly", function()
|
it("handles lack of required argument correctly", function()
|
||||||
local parser = Parser()
|
local parser = Parser()
|
||||||
parser:option "-s" "--server"
|
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)
|
end)
|
||||||
|
|
||||||
it("handles unknown options correctly", function()
|
it("handles unknown options correctly", function()
|
||||||
@@ -289,7 +290,7 @@ describe("tests related to options", function()
|
|||||||
count = 1,
|
count = 1,
|
||||||
overwrite = false
|
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)
|
end)
|
||||||
|
|
||||||
it("handles too few invocations correctly", function()
|
it("handles too few invocations correctly", function()
|
||||||
|
@@ -15,13 +15,13 @@ describe("tests related to :pparse()", function()
|
|||||||
parser:argument "foo"
|
parser:argument "foo"
|
||||||
local ok, errmsg = parser:pparse{}
|
local ok, errmsg = parser:pparse{}
|
||||||
assert.is_false(ok)
|
assert.is_false(ok)
|
||||||
assert.equal("too few arguments", errmsg)
|
assert.equal("argument 'foo' is required", errmsg)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("still raises an error if it is caused by misconfiguration", function()
|
it("rethrows errors from callbacks", function()
|
||||||
local parser = Parser()
|
local parser = Parser()
|
||||||
parser:flag "--foo"
|
parser:flag "--foo"
|
||||||
:action(error)
|
:action(function() error("some error message") end)
|
||||||
assert.has_error(function() parser:pparse{"--foo"} end)
|
assert.error_matches(function() parser:pparse{"--foo"} end, "some error message")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
752
src/argparse.lua
752
src/argparse.lua
@@ -19,7 +19,6 @@
|
|||||||
-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
-- 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.
|
-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
local function deep_update(t1, t2)
|
local function deep_update(t1, t2)
|
||||||
for k, v in pairs(t2) do
|
for k, v in pairs(t2) do
|
||||||
if type(v) == "table" then
|
if type(v) == "table" then
|
||||||
@@ -35,61 +34,63 @@ end
|
|||||||
-- A property is a tuple {name, callback}.
|
-- A property is a tuple {name, callback}.
|
||||||
-- properties.args is number of properties that can be set as arguments
|
-- properties.args is number of properties that can be set as arguments
|
||||||
-- when calling an object.
|
-- when calling an object.
|
||||||
local function new_class(prototype, properties, parent)
|
local function class(prototype, properties, parent)
|
||||||
-- Class is the metatable of its instances.
|
-- Class is the metatable of its instances.
|
||||||
local class = {}
|
local cl = {}
|
||||||
class.__index = class
|
cl.__index = cl
|
||||||
|
|
||||||
if parent then
|
if parent then
|
||||||
class.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
|
cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
|
||||||
else
|
else
|
||||||
class.__prototype = prototype
|
cl.__prototype = prototype
|
||||||
end
|
end
|
||||||
|
|
||||||
local names = {}
|
if properties then
|
||||||
|
local names = {}
|
||||||
|
|
||||||
-- Create setter methods and fill set of property names.
|
-- Create setter methods and fill set of property names.
|
||||||
for _, property in ipairs(properties) do
|
for _, property in ipairs(properties) do
|
||||||
local name, callback = property[1], property[2]
|
local name, callback = property[1], property[2]
|
||||||
|
|
||||||
class[name] = function(self, value)
|
cl[name] = function(self, value)
|
||||||
if not callback(self, value) then
|
if not callback(self, value) then
|
||||||
self["_" .. name] = value
|
self["_" .. name] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
names[name] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
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.
|
||||||
|
if type((...)) == "table" then
|
||||||
|
for name, value in pairs((...)) do
|
||||||
|
if names[name] then
|
||||||
|
self[name](self, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local nargs = select("#", ...)
|
||||||
|
|
||||||
|
for i, property in ipairs(properties) do
|
||||||
|
if i > nargs or i > properties.args then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
local arg = select(i, ...)
|
||||||
|
|
||||||
|
if arg ~= nil then
|
||||||
|
self[property[1]](self, arg)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
names[name] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
function class.__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.
|
|
||||||
if type((...)) == "table" then
|
|
||||||
for name, value in pairs((...)) do
|
|
||||||
if names[name] then
|
|
||||||
self[name](self, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local nargs = select("#", ...)
|
|
||||||
|
|
||||||
for i, property in ipairs(properties) do
|
|
||||||
if i > nargs or i > properties.args then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
local arg = select(i, ...)
|
|
||||||
|
|
||||||
if arg ~= nil then
|
|
||||||
self[property[1]](self, arg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If indexing class fails, fallback to its parent.
|
-- If indexing class fails, fallback to its parent.
|
||||||
@@ -104,7 +105,7 @@ local function new_class(prototype, properties, parent)
|
|||||||
return object(...)
|
return object(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
return setmetatable(class, class_metatable)
|
return setmetatable(cl, class_metatable)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function typecheck(name, types, value)
|
local function typecheck(name, types, value)
|
||||||
@@ -204,7 +205,7 @@ local add_help = {"add_help", function(self, value)
|
|||||||
end
|
end
|
||||||
end}
|
end}
|
||||||
|
|
||||||
local Parser = new_class({
|
local Parser = class({
|
||||||
_arguments = {},
|
_arguments = {},
|
||||||
_options = {},
|
_options = {},
|
||||||
_commands = {},
|
_commands = {},
|
||||||
@@ -223,7 +224,7 @@ local Parser = new_class({
|
|||||||
add_help
|
add_help
|
||||||
})
|
})
|
||||||
|
|
||||||
local Command = new_class({
|
local Command = class({
|
||||||
_aliases = {}
|
_aliases = {}
|
||||||
}, {
|
}, {
|
||||||
args = 3,
|
args = 3,
|
||||||
@@ -235,11 +236,10 @@ local Command = new_class({
|
|||||||
typechecked("help", "string"),
|
typechecked("help", "string"),
|
||||||
typechecked("require_command", "boolean"),
|
typechecked("require_command", "boolean"),
|
||||||
typechecked("handle_options", "boolean"),
|
typechecked("handle_options", "boolean"),
|
||||||
typechecked("action", "function"),
|
|
||||||
add_help
|
add_help
|
||||||
}, Parser)
|
}, Parser)
|
||||||
|
|
||||||
local Argument = new_class({
|
local Argument = class({
|
||||||
_minargs = 1,
|
_minargs = 1,
|
||||||
_maxargs = 1,
|
_maxargs = 1,
|
||||||
_mincount = 1,
|
_mincount = 1,
|
||||||
@@ -259,7 +259,7 @@ local Argument = new_class({
|
|||||||
typechecked("argname", "string", "table")
|
typechecked("argname", "string", "table")
|
||||||
})
|
})
|
||||||
|
|
||||||
local Option = new_class({
|
local Option = class({
|
||||||
_aliases = {},
|
_aliases = {},
|
||||||
_mincount = 0,
|
_mincount = 0,
|
||||||
_overwrite = true
|
_overwrite = true
|
||||||
@@ -322,22 +322,42 @@ function Argument:_get_usage()
|
|||||||
return usage
|
return usage
|
||||||
end
|
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._maxcount == 1 then
|
||||||
if self._maxargs == 0 then
|
if self._maxargs == 0 then
|
||||||
return "flag"
|
return self._action or actions.store_true, nil
|
||||||
elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then
|
|
||||||
return "arg"
|
|
||||||
else
|
else
|
||||||
return "multiarg"
|
return self._action or actions.store, nil
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if self._maxargs == 0 then
|
if self._maxargs == 0 then
|
||||||
return "counter"
|
return self._action or actions.count, 0
|
||||||
elseif self._maxargs == 1 and self._minargs == 1 then
|
|
||||||
return "multicount"
|
|
||||||
else
|
else
|
||||||
return "twodimensional"
|
return self._action or actions.append, {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -411,6 +431,10 @@ function Option:_get_usage()
|
|||||||
return usage
|
return usage
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Argument:_get_default_target()
|
||||||
|
return self._name
|
||||||
|
end
|
||||||
|
|
||||||
function Option:_get_default_target()
|
function Option:_get_default_target()
|
||||||
local res
|
local res
|
||||||
|
|
||||||
@@ -635,19 +659,21 @@ local function get_tip(context, wrong_name)
|
|||||||
local possible_names = {}
|
local possible_names = {}
|
||||||
|
|
||||||
for name in pairs(context) do
|
for name in pairs(context) do
|
||||||
for i=1, #name do
|
if type(name) == "string" then
|
||||||
possible_name = name:sub(1, i-1) .. name:sub(i+1)
|
for i = 1, #name do
|
||||||
|
possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
|
||||||
|
|
||||||
if not context_pool[possible_name] then
|
if not context_pool[possible_name] then
|
||||||
context_pool[possible_name] = {}
|
context_pool[possible_name] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(context_pool[possible_name], name)
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(context_pool[possible_name], name)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for i=1, #wrong_name+1 do
|
for i = 1, #wrong_name + 1 do
|
||||||
possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1)
|
possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1)
|
||||||
|
|
||||||
if context[possible_name] then
|
if context[possible_name] then
|
||||||
possible_names[possible_name] = true
|
possible_names[possible_name] = true
|
||||||
@@ -659,6 +685,7 @@ local function get_tip(context, wrong_name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local first = next(possible_names)
|
local first = next(possible_names)
|
||||||
|
|
||||||
if first then
|
if first then
|
||||||
if next(possible_names, first) then
|
if next(possible_names, first) then
|
||||||
local possible_names_arr = {}
|
local possible_names_arr = {}
|
||||||
@@ -677,350 +704,360 @@ local function get_tip(context, wrong_name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function plural(x)
|
local ElementState = class({
|
||||||
if x == 1 then
|
invocations = 0
|
||||||
return ""
|
})
|
||||||
end
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
-- Compatibility with strict.lua and other checkers:
|
function ElementState:error(fmt, ...)
|
||||||
local default_cmdline = rawget(_G, "arg") or {}
|
self.state:error(fmt, ...)
|
||||||
|
end
|
||||||
|
|
||||||
function Parser:_parse(args, errhandler)
|
function ElementState:convert(argument)
|
||||||
args = args or default_cmdline
|
local converter = self.element._convert
|
||||||
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, ...)
|
if converter then
|
||||||
return errhandler(parser, fmt:format(...))
|
local ok, err
|
||||||
end
|
|
||||||
|
|
||||||
local function assert_(assertion, ...)
|
if type(converter) == "function" then
|
||||||
return assertion or error_(...)
|
ok, err = converter(argument)
|
||||||
end
|
|
||||||
|
|
||||||
local function convert(element, data)
|
|
||||||
if element._convert then
|
|
||||||
local ok, err
|
|
||||||
|
|
||||||
if type(element._convert) == "function" then
|
|
||||||
ok, err = element._convert(data)
|
|
||||||
else
|
|
||||||
ok = element._convert[data]
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'")
|
|
||||||
data = ok
|
|
||||||
end
|
|
||||||
|
|
||||||
return data
|
|
||||||
end
|
|
||||||
|
|
||||||
local invoke, pass, close
|
|
||||||
|
|
||||||
function invoke(element)
|
|
||||||
local overwrite = false
|
|
||||||
|
|
||||||
if invocations[element] == element._maxcount then
|
|
||||||
if element._overwrite then
|
|
||||||
overwrite = true
|
|
||||||
else
|
|
||||||
error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount))
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
invocations[element] = invocations[element]+1
|
ok = converter[argument]
|
||||||
end
|
end
|
||||||
|
|
||||||
passed[element] = 0
|
if ok == nil then
|
||||||
local type_ = element:_get_type()
|
self:error(err and "%s" or "malformed argument '%s'", err or argument)
|
||||||
local target = targets[element]
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if element._maxargs == 0 then
|
argument = ok
|
||||||
close(element)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function pass(element, data)
|
return argument
|
||||||
passed[element] = passed[element]+1
|
end
|
||||||
data = convert(element, data)
|
|
||||||
local type_ = element:_get_type()
|
|
||||||
local target = targets[element]
|
|
||||||
|
|
||||||
if type_ == "arg" then
|
function ElementState:default(mode)
|
||||||
result[target] = data
|
return self.element._defmode:find(mode) and self.element._default
|
||||||
elseif type_ == "multiarg" or type_ == "multicount" then
|
end
|
||||||
table.insert(result[target], data)
|
|
||||||
elseif type_ == "twodimensional" then
|
|
||||||
table.insert(result[target][#result[target]], data)
|
|
||||||
end
|
|
||||||
|
|
||||||
if passed[element] == element._maxargs then
|
local function bound(noun, min, max, is_max)
|
||||||
close(element)
|
local res = ""
|
||||||
end
|
|
||||||
|
if min ~= max then
|
||||||
|
res = "at " .. (is_max and "most" or "least") .. " "
|
||||||
end
|
end
|
||||||
|
|
||||||
local function complete_invocation(element)
|
local number = is_max and max or min
|
||||||
while passed[element] < element._minargs do
|
return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s")
|
||||||
pass(element, element._default)
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function close(element)
|
function ElementState:invoke(alias)
|
||||||
if passed[element] < element._minargs then
|
self.open = true
|
||||||
if element._default and element._defmode:find "a" then
|
self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name)
|
||||||
complete_invocation(element)
|
self.overwrite = false
|
||||||
else
|
|
||||||
error_("too few arguments")
|
if self.invocations >= self.element._maxcount then
|
||||||
end
|
if self.element._overwrite then
|
||||||
|
self.overwrite = true
|
||||||
else
|
else
|
||||||
if element == cur_option then
|
self:error("%s must be used %s", self.name, bound("time", self.element._mincount, self.element._maxcount, true))
|
||||||
cur_option = nil
|
|
||||||
elseif element == cur_arg then
|
|
||||||
cur_arg_i = cur_arg_i+1
|
|
||||||
cur_arg = arguments[cur_arg_i]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
self.invocations = self.invocations + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local function switch(p)
|
self.args = {}
|
||||||
parser = p
|
|
||||||
|
|
||||||
for _, option in ipairs(parser._options) do
|
if self.element._maxargs <= 0 then
|
||||||
table.insert(options, option)
|
self:close()
|
||||||
|
end
|
||||||
|
|
||||||
for _, alias in ipairs(option._aliases) do
|
return self.open
|
||||||
opt_context[alias] = option
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local type_ = option:_get_type()
|
function ElementState:pass(argument)
|
||||||
targets[option] = option._target or option:_get_default_target()
|
argument = self:convert(argument)
|
||||||
|
table.insert(self.args, argument)
|
||||||
|
|
||||||
if type_ == "counter" then
|
if #self.args >= self.element._maxargs then
|
||||||
result[targets[option]] = 0
|
self:close()
|
||||||
elseif type_ == "multicount" or type_ == "twodimensional" then
|
end
|
||||||
result[targets[option]] = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
invocations[option] = 0
|
return self.open
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, mutex in ipairs(parser._mutexes) do
|
function ElementState:complete_invocation()
|
||||||
for _, option in ipairs(mutex) do
|
while #self.args < self.element._minargs do
|
||||||
if not option_mutexes[option] then
|
self:pass(self.element._default)
|
||||||
option_mutexes[option] = {mutex}
|
end
|
||||||
else
|
end
|
||||||
table.insert(option_mutexes[option], mutex)
|
|
||||||
|
function ElementState:close()
|
||||||
|
if self.open then
|
||||||
|
self.open = false
|
||||||
|
|
||||||
|
if #self.args < self.element._minargs then
|
||||||
|
if self:default("a") then
|
||||||
|
self:complete_invocation()
|
||||||
|
else
|
||||||
|
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
|
end
|
||||||
|
|
||||||
|
self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, argument in ipairs(parser._arguments) do
|
local args = self.args
|
||||||
table.insert(arguments, argument)
|
|
||||||
invocations[argument] = 0
|
if self.element._maxargs <= 1 then
|
||||||
targets[argument] = argument._target or argument._name
|
args = args[1]
|
||||||
invoke(argument)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
handle_options = parser._handle_options
|
if self.element._maxargs == 1 and self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then
|
||||||
cur_arg = arguments[cur_arg_i]
|
args = self.args
|
||||||
commands = parser._commands
|
end
|
||||||
com_context = {}
|
|
||||||
|
|
||||||
for _, command in ipairs(commands) do
|
self.action(self.result, self.target, args, self.overwrite)
|
||||||
targets[command] = command._target or command._name
|
end
|
||||||
|
end
|
||||||
|
|
||||||
for _, alias in ipairs(command._aliases) do
|
local ParseState = class({
|
||||||
com_context[alias] = command
|
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
|
||||||
|
option = ElementState(self, option)
|
||||||
|
table.insert(self.options, option)
|
||||||
|
|
||||||
|
for _, alias in ipairs(option.element._aliases) do
|
||||||
|
self.options[alias] = option
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, mutex in ipairs(parser._mutexes) do
|
||||||
|
for _, option in ipairs(mutex) do
|
||||||
|
if not self.element_to_mutexes[option] then
|
||||||
|
self.element_to_mutexes[option] = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
table.insert(self.element_to_mutexes[option], mutex)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_option(name)
|
for _, argument in ipairs(parser._arguments) do
|
||||||
return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name))
|
argument = ElementState(self, argument)
|
||||||
|
table.insert(self.arguments, argument)
|
||||||
|
argument:invoke()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function do_action(element)
|
self.handle_options = parser._handle_options
|
||||||
if element._action then
|
self.argument = self.arguments[self.argument_i]
|
||||||
element._action()
|
self.commands = parser._commands
|
||||||
|
|
||||||
|
for _, command in ipairs(self.commands) do
|
||||||
|
for _, alias in ipairs(command._aliases) do
|
||||||
|
self.commands[alias] = command
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function handle_argument(data)
|
function ParseState:get_option(name)
|
||||||
if cur_option then
|
local option = self.options[name]
|
||||||
pass(cur_option, data)
|
|
||||||
elseif cur_arg then
|
if not option then
|
||||||
pass(cur_arg, data)
|
self:error("unknown option '%s'%s", name, get_tip(self.options, name))
|
||||||
|
else
|
||||||
|
return option
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
else
|
||||||
local com = com_context[data]
|
self:error("too many arguments")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return command
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if not com then
|
function ParseState:invoke(option, name)
|
||||||
if #commands > 0 then
|
self:close()
|
||||||
error_("unknown command '%s'%s", data, get_tip(com_context, data))
|
|
||||||
else
|
if self.element_to_mutexes[option.element] then
|
||||||
error_("too many arguments")
|
for _, mutex in ipairs(self.element_to_mutexes[option.element]) do
|
||||||
|
local used_option = self.mutex_to_used_option[mutex]
|
||||||
|
|
||||||
|
if used_option and used_option ~= option then
|
||||||
|
self:error("option '%s' can not be used together with %s", name, used_option.name)
|
||||||
|
else
|
||||||
|
self.mutex_to_used_option[mutex] = option
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
end
|
||||||
else
|
else
|
||||||
result[targets[com]] = true
|
self:error("%s must be used %s", name, bound("time", mincount, option.element._maxcount))
|
||||||
do_action(com)
|
|
||||||
switch(com)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function handle_option(data)
|
function ParseState:parse(args)
|
||||||
if cur_option then
|
for _, arg in ipairs(args) do
|
||||||
close(cur_option)
|
local plain = true
|
||||||
end
|
|
||||||
|
|
||||||
cur_option = opt_context[data]
|
if self.handle_options then
|
||||||
|
local first = arg:sub(1, 1)
|
||||||
|
|
||||||
if option_mutexes[cur_option] then
|
if self.charset[first] then
|
||||||
for _, mutex in ipairs(option_mutexes[cur_option]) do
|
if #arg > 1 then
|
||||||
if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then
|
plain = false
|
||||||
error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name)
|
|
||||||
else
|
|
||||||
used_mutexes[mutex] = cur_option
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
do_action(cur_option)
|
if arg:sub(2, 2) == first then
|
||||||
invoke(cur_option)
|
if #arg == 2 then
|
||||||
end
|
self:close()
|
||||||
|
self.handle_options = false
|
||||||
local function mainloop()
|
|
||||||
|
|
||||||
for _, data 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
|
|
||||||
plain = false
|
|
||||||
if data:sub(2, 2) == first then
|
|
||||||
if #data == 2 then
|
|
||||||
if cur_option then
|
|
||||||
close(cur_option)
|
|
||||||
end
|
|
||||||
|
|
||||||
handle_options = false
|
|
||||||
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)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
for i = 2, #data do
|
local equals = arg:find "="
|
||||||
name = first .. data:sub(i, i)
|
if equals then
|
||||||
option = get_option(name)
|
local name = arg:sub(1, equals - 1)
|
||||||
handle_option(name)
|
local option = self:get_option(name)
|
||||||
|
|
||||||
if i ~= #data and option._minargs > 0 then
|
if option.element._maxargs <= 0 then
|
||||||
handle_argument(data:sub(i+1))
|
self:error("option '%s' does not take arguments", name)
|
||||||
break
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:invoke(option, name)
|
||||||
|
self:pass(arg:sub(equals + 1))
|
||||||
|
else
|
||||||
|
local option = self:get_option(arg)
|
||||||
|
self:invoke(option, arg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for i = 2, #arg do
|
||||||
|
local name = first .. arg:sub(i, i)
|
||||||
|
local option = self:get_option(name)
|
||||||
|
self:invoke(option, name)
|
||||||
|
|
||||||
|
if i ~= #arg and option.element._minargs > 0 and option.element._maxargs > 0 then
|
||||||
|
self:pass(arg:sub(i + 1))
|
||||||
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if plain then
|
if plain then
|
||||||
handle_argument(data)
|
self:pass(arg)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
switch(self)
|
self:finalize()
|
||||||
charset = parser:_update_charset()
|
return self.result
|
||||||
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)
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Parser:error(msg)
|
function Parser:error(msg)
|
||||||
@@ -1028,24 +1065,37 @@ function Parser:error(msg)
|
|||||||
os.exit(1)
|
os.exit(1)
|
||||||
end
|
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)
|
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
|
end
|
||||||
|
|
||||||
function Parser:pparse(args)
|
function Parser:pparse(args)
|
||||||
local errmsg
|
local parse_error
|
||||||
local ok, result = pcall(function()
|
|
||||||
|
local ok, result = xpcall(function()
|
||||||
return self:_parse(args, function(_, err)
|
return self:_parse(args, function(_, err)
|
||||||
errmsg = err
|
parse_error = err
|
||||||
return error()
|
error(err, 0)
|
||||||
end)
|
end)
|
||||||
end)
|
end, xpcall_error_handler)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
return true, result
|
return true, result
|
||||||
|
elseif not parse_error then
|
||||||
|
error(result, 0)
|
||||||
else
|
else
|
||||||
assert(errmsg, result)
|
return false, parse_error
|
||||||
return false, errmsg
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user