add even more sugar

This commit is contained in:
mpeterv
2014-01-23 20:02:11 +04:00
parent ba095896ac
commit c763e19b40
2 changed files with 146 additions and 135 deletions

View File

@@ -49,7 +49,7 @@ describe("tests related to commands", function()
local args = parser:parse{} local args = parser:parse{}
assert.same({}, args) assert.same({}, args)
parser.require_command = true parser:require_command(true)
assert.has_error(function() parser:parse{} end, "command is required") assert.has_error(function() parser:parse{} end, "command is required")
end) end)

View File

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