Get Opt

lua-users home
wiki

Difference (from prior major revision) (author diff)

Changed: 1,358c1
[!] VersionNotice: The below code pertains to an older Lua version, Lua 4. It does not run as is under Lua 5. There is a Lua 5 version of getopt in [stdlib].

Here's an implementation of GetOpt in Lua. It's quite flexible. It needs ReubensUtilLib.


-- getopt
-- Reuben Thomas 5/6-17/7/01
-- translated from Svenne Panne's Haskell GetOpt, as used in GHC

-- Differences between POSIX getopt and this implementation:
-- * Short options may have their argument given in the form -o=a,
-- just like long options
-- * To enforce a coherent description of options and arguments,
-- there are explanation fields in the option/argument descriptor
-- * Error messages are more informative, but not POSIX compliant
-- * Adds a wrapper processArgs to simplify usage and support common
-- options

-- Assumes prog = { name, [banner,] [purpose,] [notes]}

-- Set _DEBUG to run the example (at the end)

-- We use about 315 lines here, compared with >1100 in the glibc
-- version and 195 in the Haskell version, neither of which has the
-- same functionality

-- To do:
-- Expand argument types so that later option values can override
-- earlier (the option then has only one value field, not a list)
-- Wrap all usage messages; do all wrapping in processArgs, not
-- usageInfo

-- require("/home/rrt/lib/util.lua")


-- getOpt: perform argument processing
-- argIn: list of command-line args
-- options: options table
-- optsFirst: if true, stop processing at first non-option
-- inOrder: return opts and non-opts in order
-- returns
-- if not inOrder:
-- argOut: table of remaining non-options
-- optOut: table of option key-value list pairs
-- errors: table of error messages
-- otherwise:
-- argOut: table of option key-value pairs and non-options, in
-- parse order
-- nil: (no options)
-- errors: table of error messages
-- errors: list of error strings

function getOpt(argIn, options, optsFirst, inOrder)
local noProcess
local argOut, optOut, errors = {[0] = argIn[0]}, {}, {}

function getArg(optTy, opt, arg) -- get an argument for option opt
if optTy.arg == "None" then
if arg then tinsert(%errors, errNoArg(opt)) end
else
if not arg and %argIn[1] and strsub(%argIn[1], 1, 1) ~= "-" then
arg = %argIn[1]
tremove(%argIn, 1)
end
if not arg and optTy.arg == "Req" then
tinsert(%errors, errReqArg(opt, optTy.var))
return nil
end
end
if optTy.func then return optTy.func(arg) end
return arg or 1 -- make sure arg has some value if no default function
end

function addOpt(key, val) -- add an option to optOut or argOut
if not %inOrder then
if %optOut[key] then tinsert(%optOut[key], val)
else %optOut[key] = {val}
end
else
tinsert(%argOut, {[key] = val})
end
end

function shortOpt(opt, arg) -- parse a short option
if %options.short[opt] then
local o = %options.short[opt]
%addOpt(o.long[1], %getArg(o.type, opt, arg))
else tinsert(%errors, errUnrec(opt))
end
end

function longOpt(opt, arg) -- parse a long option
local o = findLongOpt(%options, opt)
if o.type then
%addOpt(o.long[1], %getArg(o.type, opt, arg))
elseif getn(o) > 0 then
tinsert(%errors, errAmbig(opt, o))
else
tinsert(%errors, errUnrec(opt))
end
end

while argIn[1] do
local v = argIn[1]
tremove(argIn, 1)
local _, _, dash, opt = strfind(v, "^(%-%-?)([^=-][^=]*)")
local _, _, arg = strfind(v, "=(.*)$")

if v == "--" then noProcess = 1
elseif not dash or noProcess then -- non-opt
tinsert(argOut, v)
if optsFirst then noProcess = 1 end

elseif dash == "-" and options.short[strsub(opt, 1, 1)] then -- short options
for i = 1, strlen(opt) - 1 do
shortOpt(strsub(opt, i, i))
end
shortOpt(strsub(opt, -1), arg)

elseif dash then -- long option
longOpt(opt, arg)
end
end

argOut.n = getn(argOut)
if inOrder then return argOut, nil, errors end
return argOut, optOut, errors
end


-- Options table type

Option = constructor{
"short", -- list of short names
"long", -- list of long names
"type", -- OptType
"desc", -- description of this option
}

OptType = constructor{
"arg", -- type of argument: None, Req(uired), Opt(ional)
"var", -- descriptive name for the argument
"func" -- optional function to convert argument into actual argument
-- (if omitted, argument is left as it is)
}

-- Options table constructor: adds lookup tables for the long and
-- short options
function Options(t)
local short, long = {}, {}

for i = 1, getn(t) do
for j, s in t[i].short do
if short[s] then warn("duplicate short option '" .. s .. "'") end
short[s] = t[i]
end
for j, l in t[i].long do long[l] = t[i] end
end
t.short = short
t.long = long
return t
end

-- findLongOpt: find long options corresponding to a given prefix
-- options: table to search
-- opt: option prefix
-- returns
-- match: match (if only one) or table of matches
function findLongOpt(options, opt)
local len, match = strlen(opt), {}

for i, v in options.long do
if strsub(i, 1, len) == opt then tinsert(match, v) end
end

if getn(match) == 1 then return match[1] end
return match
end


-- Error and usage information formatting

-- errAmbig: ambiguous option
-- optStr: option string
-- optTypes: option descriptions
-- returns
-- err: option error
function errAmbig(optStr, optTypes)
local header = "option `" .. optStr .. "' is ambiguous; could be one of:"
return usageInfo(header, optTypes)
end

-- errNoArg: argument when there shouldn't be one
-- optStr: option string
-- returns
-- err: option error
function errNoArg(optStr)
return "option `" .. optStr .. "' doesn't take an argument"
end

-- errReqArg: required argument missing
-- optStr: option string
-- desc: argument description
-- returns
-- err: option error
function errReqArg(optStr, desc)
return "option `" .. optStr .. "' requires an argument `" .. desc .. "'"
end

-- errUnrec: unrecognized option
-- optStr: option string
-- returns
-- err: option error
function errUnrec(optStr)
return "unrecognized option `" .. optStr .. "'"
end


-- usageInfo: produce usage info for the given options
-- header: header string
-- optDesc: option descriptors
-- pageWidth: width to format to [78]
-- returns
-- mess: formatted string
function usageInfo(header, optDesc, pageWidth)
pageWidth = pageWidth or 78

function fmtOpt(opt) -- format the usage info for a single option
-- returns {opts, desc}: short and long options, description
function fmtShort(c) return "-" .. c end

function fmtLong(o)
local arg = %opt.type.arg
if arg == "None" then return "--" .. o end
if arg == "Req" then return "--" .. o .. "=" .. %opt.type.var end
return "--" .. o .. "[=" .. %opt.type.var .. "]"
end

return {
join(", "
, { join(", ", map(fmtShort, opt.short)),
join(", ", map(fmtLong, opt.long )) }
)
, opt.desc }
end

function sameLen(xs)
local n = call(max, map(strlen, xs))
for i, v in xs do xs[i] = strsub(v .. strrep(" ", n), 1, n) end
return xs, n
end

function paste(x, y) return " " .. x .. " " .. y end

function wrapper(w, i)
return function (s) return wrap(s, %w, 0, %i) end
end

local cols = unzip(map(fmtOpt, optDesc))
local width
cols[1], width = sameLen(cols[1])
cols[2] = map(wrapper(pageWidth - width - 4, width + 4), cols[2])

return header .. "\n" .. join("\n", mapCall(paste, unzip{sameLen(cols[1]), cols[2]}) )
end


-- processArgs: simple getOpt wrapper
-- adds --version/-v and --help/-h/-? automatically; stops program
-- if there was an error or --help was used
function processArgs(options)
local totArgs = getn(arg)

options = Options(concat(options,
{ Option({"v"}, {"version"}, OptType "None",
"show program version"),
Option({"h", "?"}, {"help"}, OptType "None",
"show this help") } ))

local errors
arg, opt, errors = getOpt(arg, options)

if (opt.version or totArgs == 0) and prog.banner then
write(_STDERR, prog.banner .. "\n")
end

if getn(errors) > 0 or totArgs == 0 or opt.help then
local name
name, prog.name = prog.name, nil
local header = ""
if getn(errors) > 0 then header = join("\n", errors) .. "\n" end

die(header
.. usageInfo("Usage: "
.. name .. " "
.. (prog.usage or "[OPTION...] FILE...") .. "\n"
.. (prog.purpose or "") .. "\n"
, options)
.. ((prog.notes and "\n\n" .. prog.notes) or ""))
end
end


-- A small and hopefully enlightening example:
if _DEBUG then

options = Options{
Option({"v"}, {"verbose"}, OptType("None"),
"verbosely list files"),
Option({"V", "?"}, {"version", "release"}, OptType("None"),
"show version info"),
Option({"o"}, {"output"}, OptType("Opt", "FILE", out),
"use FILE for dump"),
Option({"n"}, {"name"}, OptType("Req", "USER"),
"only dump USER's files")
}

function out(o) return o or _STDOUT end

function test(cmdLine, optsFirst, inOrder)
local opts, nonOpts, errors = getOpt(cmdLine, options, optsFirst, inOrder)
if getn(errors) == 0 then
print("options=" .. tostring(opts)
.. " args=" .. tostring(nonOpts) .. "\n")
else
print(join("\n", errors) .. "\n"
.. usageInfo("Usage: foobar [OPTION...] FILE...", options))
end
end

-- example runs:
test({"foo", "-v"}, 1)

-- options={} args={1=foo,2=-v,n=2}
test{"foo", "-v"}

-- options={verbose={1=1}} args={1=foo,n=1}
test({"foo", "-v"}, nil, 1)

-- options=nil args={1=foo,2={verbose=1},n=2}
test{"foo", "--", "-v"}

-- options={} args={1=foo,2=-v,n=2}
test{"-?o", "--name", "bar", "--na=baz"}

-- options={output=stdout,version=1,name={1=bar,2=baz,n=2}} args={}
test{"--ver", "foo"}

-- option `--ver' is ambiguous; could be one of:
-- -v --verbose verbosely list files
-- -V, -? --version, --release show version info
-- Usage: foobar [OPTION...] files...
-- -v --verbose verbosely list files
-- -V, -? --version, --release show version info
-- -o[FILE] --output[=FILE] use FILE for dump
-- -n USER --name=USER only dump USER's files
end


There is a Lua 5 version of getopt in [stdlib].

There is a Lua 5 version of getopt in [stdlib].

See Also


RecentChanges · preferences
edit · history
Last edited March 29, 2011 3:59 pm GMT (diff)