--- Simplified getopt, based on Svenne Panne's Haskell GetOpt.
-- Usage:
--
options = Options {Option {...} ...}
-- getopt.processArgs ()
prog = {name[, banner] [, purpose] [, notes] [, usage]}
-opt=arg
or -opt arg
.Req
(uired),
-- Opt
(ional)
-- @field var descriptive name for the argument
-- @field func optional function (newarg, oldarg) to convert argument
-- into actual argument, (if omitted, argument is left as it
-- is)
_G.Option = Object {_init = {"name", "desc", "type", "var", "func"}}
--- Options table constructor: adds lookup tables for the option names
function _G.Options (t)
local name = {}
for _, v in ipairs (t) do
for j, s in pairs (v.name) do
if name[s] then
warn ("duplicate option '%s'", s)
end
name[s] = v
end
end
t.name = name
return t
end
--- Produce usage info for the given options
-- @param header header string
-- @param optDesc option descriptors
-- @param pageWidth width to format to [78]
-- @return formatted string
function usageInfo (header, optDesc, pageWidth)
pageWidth = pageWidth or 78
-- Format the usage info for a single option
-- @param opt the Option table
-- @return options
-- @return description
local function fmtOpt (opt)
local function fmtName (o)
return "-" .. o
end
local function fmtArg ()
if opt.type == nil then
return ""
elseif opt.type == "Req" then
return "=" .. opt.var
else
return "[=" .. opt.var .. "]"
end
end
local textName = list.map (fmtName, opt.name)
textName[1] = textName[1] .. fmtArg ()
return {table.concat ({table.concat (textName, ", ")}, ", "),
opt.desc}
end
local function sameLen (xs)
local n = math.max (unpack (list.map (string.len, xs)))
for i, v in pairs (xs) do
xs[i] = string.sub (v .. string.rep (" ", n), 1, n)
end
return xs, n
end
local function paste (x, y)
return " " .. x .. " " .. y
end
local function wrapper (w, i)
return function (s)
return string.wrap (s, w, i, 0)
end
end
local optText = ""
if #optDesc > 0 then
local cols = list.transpose (list.map (fmtOpt, optDesc))
local width
cols[1], width = sameLen (cols[1])
cols[2] = list.map (wrapper (pageWidth, width + 4), cols[2])
optText = "\n\n" ..
table.concat (list.mapWith (paste,
list.transpose ({sameLen (cols[1]),
cols[2]})),
"\n")
end
return header .. optText
end
--- Emit a usage message.
function usage ()
local name = prog.name
prog.name = nil
local usage, purpose, notes = "[OPTION...] FILE...", "", ""
if prog.usage then
usage = prog.usage
end
if prog.purpose then
purpose = "\n" .. prog.purpose
end
if prog.notes then
notes = "\n\n"
if not string.find (prog.notes, "\n") then
notes = notes .. string.wrap (prog.notes)
else
notes = notes .. prog.notes
end
end
warn (getopt.usageInfo ("Usage: " .. name .. " " .. usage .. purpose,
options)
.. notes)
end
--- Simple getOpt wrapper.
-- Adds -version
/-v
and
-- -help
/-h
/-?
automatically;
-- stops program if there was an error, or if -help
or
-- -version
was used.
function processArgs ()
local totArgs = #arg
options = Options (list.concat (options or {},
{Option {{"version", "v"},
"show program version"},
Option {{"help", "h", "?"},
"show this help"}}
))
local errors
_G.arg, opt, errors = getopt.getOpt (arg, options)
if (opt.version or opt.help) and prog.banner then
io.stderr:write (prog.banner .. "\n")
end
if #errors > 0 or opt.help then
local name = prog.name
prog.name = nil
if #errors > 0 then
warn (table.concat (errors, "\n") .. "\n")
end
prog.name = name
getopt.usage ()
if #errors > 0 then
error ()
end
end
if opt.version or opt.help then
os.exit ()
end
end
_G.options = nil
-- A small and hopefully enlightening example:
if type (_DEBUG) == "table" and _DEBUG.std then
function out (o)
return o or io.stdout
end
options = Options {
Option {{"verbose", "v"}, "verbosely list files"},
Option {{"version", "release", "V", "?"}, "show version info"},
Option {{"output", "o"}, "dump to FILE", "Opt", "FILE", out},
Option {{"name", "n"}, "only dump USER's files", "Req", "USER"},
}
function test (cmdLine)
local nonOpts, opts, errors = getopt.getOpt (cmdLine, options)
if #errors == 0 then
print ("options=" .. tostring (opts) ..
" args=" .. tostring (nonOpts) .. "\n")
else
print (table.concat (errors, "\n") .. "\n" ..
getopt.usageInfo ("Usage: foobar [OPTION...] FILE...",
options))
end
end
-- FIXME: Turn the following documentation into unit tests
prog = {name = "foobar"} -- in case of errors
-- example runs:
test {"foo", "-v"}
-- options={verbose=1} args={1=foo,n=1}
test {"foo", "--", "-v"}
-- options={} args={1=foo,2=-v,n=2}
test {"-o", "-?", "-name", "bar", "--name=baz"}
-- options={output=userdata(?): 0x????????,version=1,name=baz} args={}
test {"-foo"}
-- unrecognized option `foo'
-- Usage: foobar [OPTION...] FILE...
-- -verbose, -v verbosely list files
-- -version, -release, -V, -? show version info
-- -output[=FILE], -o dump to FILE
-- -name=USER, -n only dump USER's files
end