mirror of
https://github.com/TangentFoxy/argparse.git
synced 2025-07-28 11:02:20 +00:00
add even more sugar
This commit is contained in:
@@ -49,7 +49,7 @@ describe("tests related to commands", function()
|
||||
local args = parser:parse{}
|
||||
assert.same({}, args)
|
||||
|
||||
parser.require_command = true
|
||||
parser:require_command(true)
|
||||
assert.has_error(function() parser:parse{} end, "command is required")
|
||||
end)
|
||||
|
||||
|
279
src/argparse.lua
279
src/argparse.lua
@@ -5,6 +5,17 @@ local class = require "30log"
|
||||
local Declarative = {}
|
||||
|
||||
function Declarative:__init(...)
|
||||
local cl = getmetatable(self)
|
||||
|
||||
for _, field in ipairs(self._fields) do
|
||||
if not cl[field] then
|
||||
cl[field] = function(s, value)
|
||||
s["_" .. field] = value
|
||||
return s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self(...)
|
||||
end
|
||||
|
||||
@@ -15,17 +26,17 @@ function Declarative:__call(...)
|
||||
name_or_options = select(i, ...)
|
||||
|
||||
if type(name_or_options) == "string" then
|
||||
if self.aliases then
|
||||
table.insert(self.aliases, name_or_options)
|
||||
if self._aliases then
|
||||
table.insert(self._aliases, name_or_options)
|
||||
end
|
||||
|
||||
if not self.name then
|
||||
self.name = name_or_options
|
||||
if not self._name then
|
||||
self._name = name_or_options
|
||||
end
|
||||
elseif type(name_or_options) == "table" then
|
||||
for _, field in ipairs(self.fields) do
|
||||
for _, field in ipairs(self._fields) do
|
||||
if name_or_options[field] ~= nil then
|
||||
self[field] = name_or_options[field]
|
||||
self["_" .. field] = name_or_options[field]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,11 +47,11 @@ end
|
||||
|
||||
local Parser = class {
|
||||
__name = "Parser",
|
||||
arguments = {},
|
||||
options = {},
|
||||
commands = {},
|
||||
require_command = false,
|
||||
fields = {
|
||||
_arguments = {},
|
||||
_options = {},
|
||||
_commands = {},
|
||||
_require_command = false,
|
||||
_fields = {
|
||||
"name", "description", "target", "require_command",
|
||||
"action", "usage"
|
||||
}
|
||||
@@ -48,14 +59,14 @@ local Parser = class {
|
||||
|
||||
local Command = Parser:extends {
|
||||
__name = "Command",
|
||||
aliases = {}
|
||||
_aliases = {}
|
||||
}
|
||||
|
||||
local Argument = class {
|
||||
__name = "Argument",
|
||||
args = 1,
|
||||
count = 1,
|
||||
fields = {
|
||||
_args = 1,
|
||||
_count = 1,
|
||||
_fields = {
|
||||
"name", "description", "target", "args",
|
||||
"minargs", "maxargs", "default", "convert",
|
||||
"action", "usage", "argname"
|
||||
@@ -64,10 +75,10 @@ local Argument = class {
|
||||
|
||||
local Option = Argument:extends {
|
||||
__name = "Option",
|
||||
aliases = {},
|
||||
count = "?",
|
||||
overwrite = true,
|
||||
fields = {
|
||||
_aliases = {},
|
||||
_count = "?",
|
||||
_overwrite = true,
|
||||
_fields = {
|
||||
"name", "aliases", "description", "target",
|
||||
"args", "minargs", "maxargs", "count",
|
||||
"mincount", "maxcount", "default", "convert",
|
||||
@@ -77,29 +88,29 @@ local Option = Argument:extends {
|
||||
|
||||
local Flag = Option:extends {
|
||||
__name = "Flag",
|
||||
args = 0
|
||||
_args = 0
|
||||
}
|
||||
|
||||
function Argument:get_arg_usage(argname)
|
||||
argname = self.argname or argname
|
||||
argname = self._argname or argname
|
||||
local buf = {}
|
||||
local i = 1
|
||||
|
||||
while i <= math.min(self.minargs, 3) do
|
||||
while i <= math.min(self._minargs, 3) do
|
||||
table.insert(buf, argname)
|
||||
i = i+1
|
||||
end
|
||||
|
||||
while i <= math.min(self.maxargs, 3) do
|
||||
while i <= math.min(self._maxargs, 3) do
|
||||
table.insert(buf, "[" .. argname .. "]")
|
||||
i = i+1
|
||||
|
||||
if self.maxargs == math.huge then
|
||||
if self._maxargs == math.huge then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if i < self.maxargs then
|
||||
if i < self._maxargs then
|
||||
table.insert(buf, "...")
|
||||
end
|
||||
|
||||
@@ -107,48 +118,48 @@ function Argument:get_arg_usage(argname)
|
||||
end
|
||||
|
||||
function Argument:get_usage()
|
||||
if not self.usage then
|
||||
self.usage = table.concat(self:get_arg_usage("<" .. self.name .. ">"), " ")
|
||||
if not self._usage then
|
||||
self._usage = table.concat(self:get_arg_usage("<" .. self._name .. ">"), " ")
|
||||
end
|
||||
|
||||
return self.usage
|
||||
return self._usage
|
||||
end
|
||||
|
||||
function Option:get_usage()
|
||||
if not self.usage then
|
||||
self.usage = self:get_arg_usage("<" .. self.target .. ">")
|
||||
table.insert(self.usage, 1, self.name)
|
||||
self.usage = table.concat(self.usage, " ")
|
||||
if not self._usage then
|
||||
self._usage = self:get_arg_usage("<" .. self._target .. ">")
|
||||
table.insert(self._usage, 1, self._name)
|
||||
self._usage = table.concat(self._usage, " ")
|
||||
|
||||
if self.mincount == 0 then
|
||||
self.usage = "[" .. self.usage .. "]"
|
||||
if self._mincount == 0 then
|
||||
self._usage = "[" .. self._usage .. "]"
|
||||
end
|
||||
end
|
||||
|
||||
return self.usage
|
||||
return self._usage
|
||||
end
|
||||
|
||||
function Parser:argument(...)
|
||||
local argument = Argument:new(...)
|
||||
table.insert(self.arguments, argument)
|
||||
table.insert(self._arguments, argument)
|
||||
return argument
|
||||
end
|
||||
|
||||
function Parser:option(...)
|
||||
local option = Option:new(...)
|
||||
table.insert(self.options, option)
|
||||
table.insert(self._options, option)
|
||||
return option
|
||||
end
|
||||
|
||||
function Parser:flag(...)
|
||||
local flag = Flag:new(...)
|
||||
table.insert(self.options, flag)
|
||||
table.insert(self._options, flag)
|
||||
return flag
|
||||
end
|
||||
|
||||
function Parser:command(...)
|
||||
local command = Command:new(...)
|
||||
table.insert(self.commands, command)
|
||||
table.insert(self._commands, command)
|
||||
return command
|
||||
end
|
||||
|
||||
@@ -168,45 +179,45 @@ function Parser:assert(assertion, ...)
|
||||
end
|
||||
|
||||
function Parser:make_charset()
|
||||
if not self.charset then
|
||||
self.charset = {["-"] = true}
|
||||
if not self._charset then
|
||||
self._charset = {["-"] = true}
|
||||
|
||||
for _, command in ipairs(self.commands) do
|
||||
for _, command in ipairs(self._commands) do
|
||||
command:make_charset()
|
||||
|
||||
for char in pairs(command.charset) do
|
||||
self.charset[char] = true
|
||||
for char in pairs(command._charset) do
|
||||
self._charset[char] = true
|
||||
end
|
||||
end
|
||||
|
||||
for _, option in ipairs(self.options) do
|
||||
for _, alias in ipairs(option.aliases) do
|
||||
self.charset[alias:sub(1, 1)] = true
|
||||
for _, option in ipairs(self._options) do
|
||||
for _, alias in ipairs(option._aliases) do
|
||||
self._charset[alias:sub(1, 1)] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Parser:make_targets()
|
||||
for _, option in ipairs(self.options) do
|
||||
if not option.target then
|
||||
for _, alias in ipairs(option.aliases) do
|
||||
for _, option in ipairs(self._options) do
|
||||
if not option._target then
|
||||
for _, alias in ipairs(option._aliases) do
|
||||
if alias:sub(1, 1) == alias:sub(2, 2) then
|
||||
option.target = alias:sub(3)
|
||||
option._target = alias:sub(3)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
option.target = option.target or option.aliases[1]:sub(2)
|
||||
option._target = option._target or option._aliases[1]:sub(2)
|
||||
end
|
||||
|
||||
for _, argument in ipairs(self.arguments) do
|
||||
argument.target = argument.target or argument.name
|
||||
for _, argument in ipairs(self._arguments) do
|
||||
argument._target = argument._target or argument._name
|
||||
end
|
||||
|
||||
for _, command in ipairs(self.commands) do
|
||||
command.target = command.target or command.name
|
||||
for _, command in ipairs(self._commands) do
|
||||
command._target = command._target or command._name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -239,43 +250,43 @@ local function parse_boundaries(boundaries)
|
||||
end
|
||||
|
||||
function Parser:make_boundaries()
|
||||
for _, elements in ipairs{self.arguments, self.options} do
|
||||
for _, elements in ipairs{self._arguments, self._options} do
|
||||
for _, element in ipairs(elements) do
|
||||
if not element.minargs or not element.maxargs then
|
||||
element.minargs, element.maxargs = parse_boundaries(element.args)
|
||||
if not element._minargs or not element._maxargs then
|
||||
element._minargs, element._maxargs = parse_boundaries(element._args)
|
||||
end
|
||||
|
||||
if not element.mincount or not element.maxcount then
|
||||
element.mincount, element.maxcount = parse_boundaries(element.count)
|
||||
if not element._mincount or not element._maxcount then
|
||||
element._mincount, element._maxcount = parse_boundaries(element._count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Parser:make_command_names()
|
||||
for _, command in ipairs(self.commands) do
|
||||
command.name = self.name .. " " .. command.name
|
||||
for _, command in ipairs(self._commands) do
|
||||
command._name = self._name .. " " .. command._name
|
||||
end
|
||||
end
|
||||
|
||||
function Parser:make_types()
|
||||
for _, elements in ipairs{self.arguments, self.options} do
|
||||
for _, elements in ipairs{self._arguments, self._options} do
|
||||
for _, element in ipairs(elements) do
|
||||
if element.maxcount == 1 then
|
||||
if element.maxargs == 0 then
|
||||
element.type = "flag"
|
||||
elseif element.maxargs == 1 and element.minargs == 1 then
|
||||
element.type = "arg"
|
||||
if element._maxcount == 1 then
|
||||
if element._maxargs == 0 then
|
||||
element._type = "flag"
|
||||
elseif element._maxargs == 1 and element._minargs == 1 then
|
||||
element._type = "arg"
|
||||
else
|
||||
element.type = "multi-arg"
|
||||
element._type = "multi-arg"
|
||||
end
|
||||
else
|
||||
if element.maxargs == 0 then
|
||||
element.type = "counter"
|
||||
elseif element.maxargs == 1 and element.minargs == 1 then
|
||||
element.type = "multi-count"
|
||||
if element._maxargs == 0 then
|
||||
element._type = "counter"
|
||||
elseif element._maxargs == 1 and element._minargs == 1 then
|
||||
element._type = "multi-count"
|
||||
else
|
||||
element.type = "multi-count multi-arg"
|
||||
element._type = "multi-count multi-arg"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -292,17 +303,17 @@ function Parser:prepare()
|
||||
end
|
||||
|
||||
function Parser:get_usage()
|
||||
if not self.usage then
|
||||
local buf = {"Usage:", self.name}
|
||||
if not self._usage then
|
||||
local buf = {"Usage:", self._name}
|
||||
|
||||
for _, elements in ipairs{self.options, self.arguments} do
|
||||
for _, elements in ipairs{self._options, self._arguments} do
|
||||
for _, element in ipairs(elements) do
|
||||
table.insert(buf, element:get_usage())
|
||||
end
|
||||
end
|
||||
|
||||
if #self.commands > 0 then
|
||||
if self.require_command then
|
||||
if #self._commands > 0 then
|
||||
if self._require_command then
|
||||
table.insert(buf, "<command>")
|
||||
else
|
||||
table.insert(buf, "[<command>]")
|
||||
@@ -312,15 +323,15 @@ function Parser:get_usage()
|
||||
end
|
||||
|
||||
-- TODO: prettify
|
||||
self.usage = table.concat(buf, " ")
|
||||
self._usage = table.concat(buf, " ")
|
||||
end
|
||||
|
||||
return self.usage
|
||||
return self._usage
|
||||
end
|
||||
|
||||
function Parser:parse(args)
|
||||
args = args or arg
|
||||
self.name = self.name or args[0]
|
||||
self._name = self._name or args[0]
|
||||
|
||||
local parser
|
||||
local charset
|
||||
@@ -338,8 +349,8 @@ function Parser:parse(args)
|
||||
local cur_arg
|
||||
|
||||
local function convert(element, data)
|
||||
if element.convert then
|
||||
local ok, err = element.convert(data)
|
||||
if element._convert then
|
||||
local ok, err = element._convert(data)
|
||||
|
||||
return parser:assert(ok, "%s", err or "malformed argument " .. data)
|
||||
else
|
||||
@@ -352,11 +363,11 @@ function Parser:parse(args)
|
||||
function invoke(element)
|
||||
local overwrite = false
|
||||
|
||||
if invocations[element] == element.maxcount then
|
||||
if element.overwrite then
|
||||
if invocations[element] == element._maxcount then
|
||||
if element._overwrite then
|
||||
overwrite = true
|
||||
else
|
||||
parser:error("option %s must be used at most %d times", element.name, element.maxcount)
|
||||
parser:error("option %s must be used at most %d times", element._name, element._maxcount)
|
||||
end
|
||||
else
|
||||
invocations[element] = invocations[element]+1
|
||||
@@ -364,27 +375,27 @@ function Parser:parse(args)
|
||||
|
||||
passed[element] = 0
|
||||
|
||||
if element.type == "flag" then
|
||||
result[element.target] = true
|
||||
elseif element.type == "multi-arg" then
|
||||
result[element.target] = {}
|
||||
elseif element.type == "counter" then
|
||||
if element._type == "flag" then
|
||||
result[element._target] = true
|
||||
elseif element._type == "multi-arg" then
|
||||
result[element._target] = {}
|
||||
elseif element._type == "counter" then
|
||||
if not overwrite then
|
||||
result[element.target] = result[element.target]+1
|
||||
result[element._target] = result[element._target]+1
|
||||
end
|
||||
elseif element.type == "multi-count" then
|
||||
elseif element._type == "multi-count" then
|
||||
if overwrite then
|
||||
table.remove(result[element.target], 1)
|
||||
table.remove(result[element._target], 1)
|
||||
end
|
||||
elseif element.type == "multi-count multi-arg" then
|
||||
table.insert(result[element.target], {})
|
||||
elseif element._type == "multi-count multi-arg" then
|
||||
table.insert(result[element._target], {})
|
||||
|
||||
if overwrite then
|
||||
table.remove(result[element.target], 1)
|
||||
table.remove(result[element._target], 1)
|
||||
end
|
||||
end
|
||||
|
||||
if element.maxargs == 0 then
|
||||
if element._maxargs == 0 then
|
||||
close(element)
|
||||
end
|
||||
end
|
||||
@@ -393,24 +404,24 @@ function Parser:parse(args)
|
||||
passed[element] = passed[element]+1
|
||||
data = convert(element, data)
|
||||
|
||||
if element.type == "arg" then
|
||||
result[element.target] = data
|
||||
elseif element.type == "multi-arg" or element.type == "multi-count" then
|
||||
table.insert(result[element.target], data)
|
||||
elseif element.type == "multi-count multi-arg" then
|
||||
table.insert(result[element.target][#result[element.target]], data)
|
||||
if element._type == "arg" then
|
||||
result[element._target] = data
|
||||
elseif element._type == "multi-arg" or element._type == "multi-count" then
|
||||
table.insert(result[element._target], data)
|
||||
elseif element._type == "multi-count multi-arg" then
|
||||
table.insert(result[element._target][#result[element._target]], data)
|
||||
end
|
||||
|
||||
if passed[element] == element.maxargs then
|
||||
if passed[element] == element._maxargs then
|
||||
close(element)
|
||||
end
|
||||
end
|
||||
|
||||
function close(element)
|
||||
if passed[element] < element.minargs then
|
||||
if element.default then
|
||||
while passed[element] < element.minargs do
|
||||
pass(element, element.default)
|
||||
if passed[element] < element._minargs then
|
||||
if element._default then
|
||||
while passed[element] < element._minargs do
|
||||
pass(element, element._default)
|
||||
end
|
||||
else
|
||||
parser:error("too few arguments")
|
||||
@@ -423,11 +434,11 @@ function Parser:parse(args)
|
||||
cur_arg = arguments[cur_arg_i]
|
||||
end
|
||||
|
||||
if element.action then
|
||||
if element.type == "multi-count" or element.type == "multi-count multi-arg" then
|
||||
element.action(result[element.target][#result[element.target]])
|
||||
if element._action then
|
||||
if element._type == "multi-count" or element._type == "multi-count multi-arg" then
|
||||
element._action(result[element._target][#result[element._target]])
|
||||
else
|
||||
element.action(result[element.target])
|
||||
element._action(result[element._target])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -435,40 +446,40 @@ function Parser:parse(args)
|
||||
|
||||
local function switch(p)
|
||||
parser = p:prepare()
|
||||
charset = p.charset
|
||||
charset = parser._charset
|
||||
|
||||
if p.action then
|
||||
table.insert(com_callbacks, p.action)
|
||||
if parser._action then
|
||||
table.insert(com_callbacks, parser._action)
|
||||
end
|
||||
|
||||
for _, option in ipairs(p.options) do
|
||||
for _, option in ipairs(parser._options) do
|
||||
table.insert(options, option)
|
||||
|
||||
for _, alias in ipairs(option.aliases) do
|
||||
for _, alias in ipairs(option._aliases) do
|
||||
opt_context[alias] = option
|
||||
end
|
||||
|
||||
if option.type == "counter" then
|
||||
result[option.target] = 0
|
||||
elseif option.type == "multi-count" or option.type == "multi-count multi-arg" then
|
||||
result[option.target] = {}
|
||||
if option._type == "counter" then
|
||||
result[option._target] = 0
|
||||
elseif option._type == "multi-count" or option._type == "multi-count multi-arg" then
|
||||
result[option._target] = {}
|
||||
end
|
||||
|
||||
invocations[option] = 0
|
||||
end
|
||||
|
||||
for _, argument in ipairs(p.arguments) do
|
||||
for _, argument in ipairs(parser._arguments) do
|
||||
table.insert(arguments, argument)
|
||||
invocations[argument] = 0
|
||||
invoke(argument)
|
||||
end
|
||||
|
||||
cur_arg = arguments[cur_arg_i]
|
||||
commands = p.commands
|
||||
commands = parser._commands
|
||||
com_context = {}
|
||||
|
||||
for _, command in ipairs(p.commands) do
|
||||
for _, alias in ipairs(command.aliases) do
|
||||
for _, command in ipairs(commands) do
|
||||
for _, alias in ipairs(command._aliases) do
|
||||
com_context[alias] = command
|
||||
end
|
||||
end
|
||||
@@ -489,7 +500,7 @@ function Parser:parse(args)
|
||||
parser:error("too many arguments")
|
||||
end
|
||||
else
|
||||
result[com.target] = true
|
||||
result[com._target] = true
|
||||
switch(com)
|
||||
end
|
||||
end
|
||||
@@ -523,7 +534,7 @@ function Parser:parse(args)
|
||||
option = parser:assert(opt_context[name], "unknown option %s", name)
|
||||
handle_option(name)
|
||||
|
||||
if i ~= #data and option.minargs > 0 then
|
||||
if i ~= #data and option._minargs > 0 then
|
||||
handle_argument(data:sub(i+1))
|
||||
break
|
||||
end
|
||||
@@ -539,7 +550,7 @@ function Parser:parse(args)
|
||||
if equal then
|
||||
name = data:sub(1, equal-1)
|
||||
option = parser:assert(opt_context[name], "unknown option %s", name)
|
||||
parser:assert(option.maxargs > 0, "option %s doesn't take arguments", name)
|
||||
parser:assert(option._maxargs > 0, "option %s doesn't take arguments", name)
|
||||
|
||||
handle_option(data:sub(1, equal-1))
|
||||
handle_argument(data:sub(equal+1))
|
||||
@@ -570,13 +581,13 @@ function Parser:parse(args)
|
||||
close(cur_arg)
|
||||
end
|
||||
|
||||
if parser.require_command and #commands > 0 then
|
||||
if parser._require_command and #commands > 0 then
|
||||
parser:error("command is required")
|
||||
end
|
||||
|
||||
for _, option in ipairs(options) do
|
||||
parser:assert(invocations[option] >= option.mincount,
|
||||
"option %s must be used at least %d times", option.name, option.mincount
|
||||
parser:assert(invocations[option] >= option._mincount,
|
||||
"option %s must be used at least %d times", option._name, option._mincount
|
||||
)
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user