-------------------------------------------------------------------------------- -- Command Line OPTionS handler -- ============================ -- -- This lib generates parsers for command-line options. It encourages -- the following of some common idioms: I'm pissed off by Unix tools -- which sometimes will let you concatenate single letters options, -- sometimes won't, will prefix long name options with simple dashes -- instead of doubles, etc. -- -------------------------------------------------------------------------------- -- TODO: -- * add a generic way to unparse options ('grab everything') -- * doc -- * when a short options that takes a param isn't the last element of a series -- of shorts, take the remaining of the sequence as that param, e.g. -Ifoo -- * let unset strings/numbers with + -- * add a ++ long counterpart to + -- -{ extension 'match' } function clopts(cfg) local short, long, param_func = { }, { } local legal_types = table.transpose{ 'boolean','string','number','string*','number*','nil', '*' } ----------------------------------------------------------------------------- -- Fill short and long name indexes, and check its validity ----------------------------------------------------------------------------- for x in ivalues(cfg) do local xtype = type(x) if xtype=='table' then if not x.type then x.type='nil' end if not legal_types[x.type] then error ("Invalid type name "..x.type) end if x.short then if short[x.short] then error ("multiple definitions for option "..x.short) else short[x.short] = x end end if x.long then if long[x.long] then error ("multiple definitions for option "..x.long) else long[x.long] = x end end elseif xtype=='function' then if param_func then error "multiple parameters handler in clopts" else param_func=x end end end ----------------------------------------------------------------------------- -- Print a help message, summarizing how to use the command line ----------------------------------------------------------------------------- local function print_usage(msg) if msg then print(msg,'\n') end print(cfg.usage or "Options:\n") for x in values(cfg) do if type(x) == 'table' then local opts = { } if x.type=='boolean' then if x.short then opts = { '-'..x.short..'/+'..x.short } end if x.long then table.insert (opts, '--'..x.long..'/++'..x.long) end else if x.short then opts = { '-'..x.short..' <'..x.type..'>' } end if x.long then table.insert (opts, '--'..x.long..' <'..x.type..'>' ) end end printf(" %s: %s", table.concat(opts,', '), x.usage or '') end end print'' end -- Unless overridden, -h and --help display the help msg local default_help = { action = | | print_usage() or os.exit(0); long='help';short='h';type='nil'} if not short.h then short.h = default_help end if not long.help then long.help = default_help end ----------------------------------------------------------------------------- -- Helper function for options parsing. Execute the attached action and/or -- register the config in cfg. -- -- * cfg is the table which registers the options -- * dict the name->config entry hash table that describes options -- * flag is the prefix '-', '--' or '+' -- * opt is the option name -- * i the current index in the arguments list -- * args is the arguments list ----------------------------------------------------------------------------- local function actionate(cfg, dict, flag, opt, i, args) local entry = dict[opt] if not entry then print_usage ("invalid option "..flag..opt); return false; end local etype, name = entry.type, entry.name or entry.long or entry.short match etype with | 'string' | 'number' | 'string*' | 'number*' -> if flag=='+' or flag=='++' then print_usage ("flag "..flag.." is reserved for boolean options, not for "..opt) return false end local arg = args[i+1] if not arg then print_usage ("missing parameter for option "..flag..opt) return false end if etype:strmatch '^number' then arg = tonumber(arg) if not arg then print_usage ("option "..flag..opt.." expects a number argument") end end if etype:strmatch '%*$' then if not cfg[name] then cfg[name]={ } end table.insert(cfg[name], arg) else cfg[name] = arg end if entry.action then entry.action(arg) end return i+2 | 'boolean' -> local arg = flag=='-' or flag=='--' cfg[name] = arg if entry.action then entry.action(arg) end return i+1 | 'nil' -> cfg[name] = true; if entry.action then entry.action() end return i+1 | '*' -> local arg = table.isub(args, i+1, #args) cfg[name] = arg if entry.action then entry.action(arg) end return #args+1 | _ -> assert( false, 'undetected bad type for clopts action') end end ----------------------------------------------------------------------------- -- Parse a list of commands: the resulting function ----------------------------------------------------------------------------- local function parse(...) local cfg = { } if not ... then return cfg end local args = type(...)=='table' and ... or {...} local i, i_max = 1, #args while i <= i_max do local arg, flag, opt, opts = args[i] --printf('beginning of loop: i=%i/%i, arg=%q', i, i_max, arg) if arg=='-' then i=actionate (cfg, short, '-', '', i, args) -{ `Goto 'continue' } end ----------------------------------------------------------------------- -- double dash option ----------------------------------------------------------------------- flag, opt = arg:strmatch "^(%-%-)(.*)" if opt then i=actionate (cfg, long, flag, opt, i, args) -{ `Goto 'continue' } end ----------------------------------------------------------------------- -- double plus option ----------------------------------------------------------------------- flag, opt = arg:strmatch "^(%+%+)(.*)" if opt then i=actionate (cfg, long, flag, opt, i, args) -{ `Goto 'continue' } end ----------------------------------------------------------------------- -- single plus or single dash series of short options ----------------------------------------------------------------------- flag, opts = arg:strmatch "^([+-])(.+)" if opts then local j_max, i2 = opts:len() for j = 1, j_max do opt = opts:sub(j,j) --printf ('parsing short opt %q', opt) i2 = actionate (cfg, short, flag, opt, i, args) if i2 ~= i+1 and j < j_max then error ('short option '..opt..' needs a param of type '..short[opt]) end end i=i2 -{ `Goto 'continue' } end ----------------------------------------------------------------------- -- handler for non-option parameter ----------------------------------------------------------------------- if param_func then param_func(args[i]) end if cfg.params then table.insert(cfg.params, args[i]) else cfg.params = { args[i] } end i=i+1 -{ `Label 'continue' } if not i then return false end end -- return cfg end return parse end