Get Opt

lua-users home
wiki

[!] 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], but that seems to have problems [1].

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
       

See Also


FindPage · RecentChanges · preferences
edit · history
Last edited April 14, 2008 9:52 am GMT (diff)