elua: more getopt.lua features

Argument count limit is now supported. The source has been cleaned up and
unified so that less code repeating is done. The module is now environment
safe, not depending on specific metatable being set on strings. More callbacks
have been added and error messages are now more descriptive.
This commit is contained in:
Daniel Kolesa 2015-01-05 10:48:46 +00:00
parent 3be1c0b7cf
commit c78fc161cf
1 changed files with 113 additions and 91 deletions

View File

@ -4,8 +4,7 @@
documentation.
TODO:
- arguments that can only be specified once (for now you can check
that manually by going over array values of opts)
- mutually exclusive groups
- i18n support
Copyright (c) 2014 Daniel "q66" Kolesa <quaker66@gmail.com>
@ -29,10 +28,20 @@
DEALINGS IN THE SOFTWARE.
]]
local arg = _G.arg -- Capture global 'arg'
local M = {}
local prefixes = { "-", "--" }
local ssub, sfind, sgsub, sformat, smatch = string.sub, string.find,
string.gsub, string.format,
string.match
local slower, supper, srep = string.lower, string.upper, string.rep
local unpack = table.unpack or unpack
local ac_process_name = function(np, nm)
if not nm or #nm < 2 then
return
@ -44,7 +53,7 @@ local ac_process_name = function(np, nm)
np[nm] = { nm }
end
for i = 1, #nm do
local pnm = nm:sub(1, i - 1) .. nm:sub(i + 1)
local pnm = ssub(nm, 1, i - 1) .. ssub(nm, i + 1)
local t = np[pnm]
if not t then
t = {}
@ -67,7 +76,7 @@ local get_autocorrect = function(descs, wrong, vi)
end
for i = 1, #wrong do
local nm = wrong:sub(1, i - 1) .. wrong:sub(i + 1)
local nm = ssub(wrong, 1, i - 1) .. ssub(wrong, i + 1)
local inp = np[nm]
if inp then
if inp == true then
@ -114,34 +123,24 @@ local is_arg = function(opt, j, descs)
return false
end
local parse_l = function(opts, opt, descs, args, parser)
local optval
local i = opt:find("=")
if i then
opt, optval = opt:sub(1, i - 1), opt:sub(i + 1)
end
local desc = get_desc(opt, 2, descs)
local argr = desc[3]
if argr or argr == nil then
if not optval then
if #args == 0 then
if argr then
error("option --" .. opt .. " requires an argument", 0)
end
elseif argr or not is_arg(args[1], 2, descs) then
optval = table.remove(args, 1)
end
end
elseif optval then
error("option --" .. opt .. " cannot have an argument", 0)
end
local write_arg = function(desc, j, opts, opt, optval, parser, argcounts)
local rets
if desc.callback then
rets = { desc:callback(parser, optval, opts) }
end
if not rets or #rets == 0 then rets = { optval } end
local optn = desc.alias or desc[1] or desc[2]
local cnt = desc.max_count or (desc.list and -1 or 1)
local acnt = argcounts[optn]
if acnt then
if cnt >= 0 and acnt >= cnt then
error("option " .. prefixes[j] .. opt
.. " can be specified at most " .. cnt .. " times", 0)
end
argcounts[optn] = acnt + 1
else
argcounts[optn] = 1
end
opts[#opts + 1] = { optn, short = desc[1], long = desc[2],
alias = desc.alias, val = optval, unpack(rets) }
local optret = #rets > 1 and rets or rets[1]
@ -160,62 +159,67 @@ local parse_l = function(opts, opt, descs, args, parser)
end
end
local parse_s = function(opts, optstr, descs, args, parser)
while optstr ~= "" do
local optval
local opt = optstr:sub(1, 1)
optstr = optstr:sub(2)
local desc = get_desc(opt, 1, descs)
local argr = desc[3]
if argr or argr == nil then
if optstr == "" then
optstr = nil
if #args == 0 then
if argr then
error("option -" .. opt .. " requires an argument", 0)
end
elseif argr or not is_arg(args[1], 1, descs) then
optstr = table.remove(args, 1)
end
end
optval, optstr = optstr, ""
end
local rets
if desc.callback then
rets = { desc:callback(parser, optval, opts) }
end
if not rets or #rets == 0 then rets = { optval } end
local optn = desc.alias or desc[1] or desc[2]
opts[#opts + 1] = { optn, short = desc[1], long = desc[2],
alias = desc.alias, val = optval, unpack(rets) }
local optret = #rets > 1 and rets or rets[1]
if desc.list then
desc.list[#desc.list + 1] = optret
opts[optn] = desc.list
elseif optret ~= nil then
opts[optn] = optret
else
opts[optn] = true
end
local dopts = desc.opts
if dopts then
dopts[#dopts + 1] = opts[#opts]
dopts[optn] = opts[optn ]
end
local parse_l = function(opts, opt, descs, args, parser, argcounts)
local optval
local i = sfind(opt, "=")
if i then
opt, optval = ssub(opt, 1, i - 1), ssub(opt, i + 1)
end
local desc = get_desc(opt, 2, descs)
local argr = desc[3]
if argr or argr == nil then
if not optval then
if #args == 0 then
if argr then
error("option --" .. opt .. " requires an argument", 0)
end
elseif argr or not is_arg(args[1], 2, descs) then
optval = table.remove(args, 1)
end
end
elseif optval then
error("option --" .. opt .. " cannot have an argument", 0)
end
write_arg(desc, 2, opts, opt, optval, parser, argcounts)
end
local parse_s = function(opts, optstr, descs, args, parser, argcounts)
local opt = ssub(optstr, 1, 1)
local optval = ssub(optstr, 2)
local desc = get_desc(opt, 1, descs)
local argr = desc[3]
if argr or argr == nil then
if optval == "" then
optval = nil
if #args == 0 then
if argr then
error("option -" .. opt .. " requires an argument", 0)
end
elseif argr or not is_arg(args[1], 1, descs) then
optval = table.remove(args, 1)
end
end
elseif optval ~= "" then
error("option -" .. opt .. " cannot have an argument", 0)
else
optval = nil
end
write_arg(desc, 1, opts, opt, optval, parser, argcounts)
end
local getopt_u = function(parser)
local args = { unpack(parser.args) }
local argcounts = {}
local args = { unpack(parser.args or arg) }
local descs = parser.descs
local opts = {}
while #args > 0 and args[1]:sub(1, 1) == "-" and args[1] ~= "-" do
while #args > 0 and ssub(args[1], 1, 1) == "-" and args[1] ~= "-" do
local v = table.remove(args, 1)
if v == "--" then break end
if v:sub(1, 2) == "--" then
parse_l(opts, v:sub(3), descs, args, parser)
if ssub(v, 1, 2) == "--" then
parse_l(opts, ssub(v, 3), descs, args, parser, argcounts)
else
parse_s(opts, v:sub(2), descs, args, parser)
parse_s(opts, ssub(v, 2), descs, args, parser, argcounts)
end
end
return opts, args
@ -293,7 +297,7 @@ end
A description is represented by a table. The table has this layout:
{ shortn, longn, optional, help = helpmsg, metavar = metavar,
alias = alias, callback = retcb, list = list
alias = alias, callback = retcb, list = list, max_count = max_count
}
"shortn" refers to the short name. For example if you want your argument
@ -324,6 +328,10 @@ end
times, the list will contain all the values provided. The mapping opts[n]
will refer to the list rather than the last value given like without list.
The field "max_count" can be used to specify a limit on how many arguments
can be provided. Its implicit value is 1, unless a list is provided,
in which case it's -1 (which is a value for infinity here).
A description can also be used to specify a category, purely for help
listing purposes:
@ -349,7 +357,7 @@ end
local parse = M.parse
local repl_prog = function(str, progn)
return (str:gsub("%f[%%]%%prog", progn):gsub("%%%%prog", "%%prog"))
return (sgsub(sgsub(str, "%f[%%]%%prog", progn), "%%%%prog", "%%prog"))
end
local buf_write = function(self, ...)
@ -360,7 +368,7 @@ end
local get_metavar = function(desc)
local mv = desc.metavar
if not mv and (desc[3] or desc[3] == nil) then
mv = desc[2] and desc[2]:upper() or "VAL"
mv = desc[2] and supper(desc[2]) or "VAL"
elseif desc[3] == false then
mv = nil
end
@ -369,11 +377,11 @@ end
local help = function(parser, f, category)
local usage = parser.usage
local progn = parser.prog or parser.args[0] or "program"
local progn = parser.prog or (parser.args or arg)[0] or "program"
if usage then
usage = repl_prog(usage, progn)
else
usage = ("Usage: %s [OPTIONS]"):format(progn)
usage = sformat("Usage: %s [OPTIONS]", progn)
end
local buf = { write = buf_write }
buf:write(usage, "\n")
@ -418,12 +426,12 @@ local help = function(parser, f, category)
local sdf = lls - sln
if desc[2] then ln[#ln + 1] = ", " end
if sdf > 0 then
ln[#ln + 1] = (" "):rep(sdf)
ln[#ln + 1] = srep(" ", sdf)
end
elseif not desc[2] and mv then
ln[#ln + 1] = mv
else
ln[#ln + 1] = (" "):rep(lls + 2)
ln[#ln + 1] = srep(" ", lls + 2)
end
if desc[2] then
ln[#ln + 1] = "--" .. desc[2]
@ -434,10 +442,10 @@ local help = function(parser, f, category)
lln = math.max(lln, #ln)
lns[#lns + 1] = { ln, desc.help }
elseif nign and desc.category then
local lcat = category and category:lower() or nil
local alias = desc.alias and desc.alias:lower() or nil
local lcat = category and slower(category) or nil
local alias = desc.alias and slower(desc.alias) or nil
iscat = (not category) or (alias == lcat)
or (desc.category:lower() == lcat)
or (slower(desc.category) == lcat)
if iscat then
wascat = true
lns[#lns + 1] = { false, desc.category }
@ -514,7 +522,7 @@ end
The header is printed only when given as part of the parser.
The "The following optins are supported:" line is only printed when there
The "The following options are supported:" line is only printed when there
are options to print.
Same goes for the footer as for the header.
@ -534,33 +542,47 @@ M.help = function(parser, category, f)
return pcall(help, parser, f or io.stderr, category)
end
-- A utility callback to parse a number
-- If a 'base' field is present in the description, uses that.
M.number_cb = function(desc, parser, v)
local n = tonumber(v, desc.base)
if not n then
error("bad number value: " .. v, 0)
end
return n
end
-- A utility callback for geometry parsing (--foo=x:y:w:h).
M.geometry_parse_cb = function(desc, parser, v)
local x, y, w, h = v:match("^(%d+):(%d+):(%d+):(%d+)$")
local x, y, w, h = smatch(v, "^(%d+):(%d+):(%d+):(%d+)$")
if not x then
error("bad geometry value: " .. v, 0)
error("bad geometry value (X:Y:W:H expected): " .. v, 0)
end
return x, y, w, h
return tonumber(x), tonumber(y), tonumber(w), tonumber(h)
end
-- A utility callback for size parsing (--foo=WxH).
M.size_parse_cb = function(desc, parser, v)
local w, h = v:match("^(%d+)x(%d+)$")
local w, h = smatch(v, "^(%d+)x(%d+)$")
if not w then
error("bad size value: " .. v, 0)
error("bad size value (WxH expected): " .. v, 0)
end
return w, h
return tonumber(w), tonumber(h)
end
-- A utility callback generator for help. Returns a utility callback when
-- called with file stream as an argument (optional, defaults to stderr).
-- If the second argument is true, exits the program with successful exit code.
-- For help args that take a value, the value will be used as a category name.
M.help_cb = function(fstream)
M.help_cb = function(fstream, exit)
return function(desc, parser, v)
local succ, err = M.help(parser, v, fstream)
if not succ then
error(err, 0)
end
if exit then
os.exit(0, true) -- need 0 for lua 5.1
end
end
end