-- lapp.lua -- Simple command-line parsing using human-readable specification -- -- $Id: xindex-lapp.lua 12 2021-08-15 09:28:32Z hvoss $ ----------------------------- --~ -- args.lua --~ local args = require ('lapp') [[ --~ Testing parameter handling --~ -p Plain flag (defaults to false) --~ -q,--quiet Plain flag with GNU-style optional long name --~ -o (string) Required string option --~ -n (number) Required number option --~ -s (default 1.0) Option that takes a number, but will default --~ (number) Required number argument --~ (default stdin) A parameter which is an input file --~ (default stdout) One that is an output file --~ ]] --~ for k,v in pairs(args) do --~ print(k,v) --~ end ------------------------------- --~ > args -pq -o help -n 2 2.3 --~ input file (781C1B78) --~ p true --~ s 1 --~ output file (781C1B98) --~ quiet true --~ start 2.3 --~ o help --~ n 2 -------------------------------- if not modules then modules = { } end modules ['xindex-lapp'] = { version = 0.31, comment = "read parameter for xindex.lua", author = "Steve Donovan", copyright = "Steve Donovan", license = "MIT" } lapp = {} local append = table.insert local usage local open_files = {} local parms = {} local aliases = {} local parmlist = {} local filetypes = { stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'}, stderr = {io.stderr,'file-out'} } local function quit(msg,no_usage) if msg then io.stderr:write(msg..'\n\n') end if not no_usage then io.stderr:write(usage) end os.exit(1); end local function error(msg,no_usage) quit(arg[0]:gsub('.+[\\/]','')..':'..msg,no_usage) end local function ltrim(line) return line:gsub('^%s*','') end local function rtrim(line) return line:gsub('%s*$','') end local function trim(s) return ltrim(rtrim(s)) end --- open a file. -- This will quit on error, and keep a list of file objects for later cleanup. -- @string file filename -- @string[opt] opt same as second parameter of `io.open` function open (file,opt) local val,err = io.open(file,opt) if not val then file = file..".idx" val,err = io.open(file,opt) if not val then error(err,true) end end append(open_files,val) return val end local function xassert(condn,msg) if not condn then error(msg) end end local function range_check(x,min,max,parm) xassert(min <= x and max >= x,parm..' out of range') end local function xtonumber(s) local val = tonumber(s) if not val then error("unable to convert to number: "..s) end return val end local function is_filetype(type) return type == 'file-in' or type == 'file-out' end local types = {} local function convert_parameter(ps,val) if ps.converter then val = ps.converter(val) end if ps.type == 'number' then val = xtonumber(val) elseif is_filetype(ps.type) then val = open(val,(ps.type == 'file-in' and 'r') or 'w' ) elseif ps.type == 'boolean' then val = true end if ps.constraint then ps.constraint(val) end return val end function lapp.add_type (name,converter,constraint) types[name] = {converter=converter,constraint=constraint} end local function force_short(short) xassert(#short==1,short..": short parameters should be one character") end function process_options_string(str) local res = {} local varargs local function check_varargs(s) local res,cnt = s:gsub('%.%.%.$','') varargs = cnt > 0 return res end local function set_result(ps,parm,val) if not ps.varargs then res[parm] = val else if not res[parm] then res[parm] = { val } else append(res[parm],val) end end end usage = str for line in str:gmatch('([^\n]*)\n') do local optspec,optparm,i1,i2,defval,vtype,constraint line = ltrim(line) -- flags: either - or -, i1,i2,optspec = line:find('^%-(%S+)') if i1 then optspec = check_varargs(optspec) local short,long = optspec:match('([^,]+),(.+)') if short then optparm = long:sub(3) aliases[short] = optparm force_short(short) else optparm = optspec force_short(optparm) end else -- is it ? i1,i2,optparm = line:find('(%b<>)') if i1 then -- so becomes input_file ... optparm = check_varargs(optparm:sub(2,-2)):gsub('%A','_') append(parmlist,optparm) end end if i1 then -- this is not a pure doc line local last_i2 = i2 local sval line = ltrim(line:sub(i2+1)) -- do we have (default ) or ()? i1,i2,typespec = line:find('^%s*(%b())') if i1 then typespec = trim(typespec:sub(2,-2)) -- trim the parens and any space sval = typespec:match('default%s+(.+)') if sval then local val = tonumber(sval) if val then -- we have a number! defval = val vtype = 'number' elseif filetypes[sval] then local ft = filetypes[sval] defval = ft[1] vtype = ft[2] else defval = sval vtype = 'string' end else local min,max = typespec:match '([^%.]+)%.%.(.+)' if min then -- it's (min..max) vtype = 'number' min = xtonumber(min) max = xtonumber(max) constraint = function(x) range_check(x,min,max,optparm) end else -- () just contains type of required parameter vtype = typespec end end else -- must be a plain flag, no extra parameter required defval = false vtype = 'boolean' end local ps = { type = vtype, defval = defval, required = defval == nil, comment = line:sub((i2 or last_i2)+1) or optparm, constraint = constraint, varargs = varargs } if types[vtype] then local converter = types[vtype].converter if type(converter) == 'string' then ps.type = converter else ps.converter = converter end ps.constraint = types[vtype].constraint end parms[optparm] = ps end end -- cool, we have our parms, let's parse the command line args local iparm = 1 local iextra = 1 local i = 1 local parm,ps,val while i <= #arg do -- look for a flag, - or -- local i1,i2,dash,parmstr = arg[i]:find('^(%-+)(%a.*)') if i1 then -- we have a flag if #dash == 2 then -- long option parm = parmstr else -- short option if #parmstr == 1 then parm = parmstr else -- multiple flags after a '-',? parm = parmstr:sub(1,1) if parmstr:find('^%a%d+') then -- a short option followed by a digit? (exception for AW ;)) -- push ahead into the arg array table.insert(arg,i+1,parmstr:sub(2)) else -- push multiple flags into the arg array! for k = 2,#parmstr do table.insert(arg,i+k-1,'-'..parmstr:sub(k,k)) end end end end if parm == 'h' or parm == 'help' then quit() end if aliases[parm] then parm = aliases[parm] end ps = parms[parm] if not ps then error("unrecognized parameter: "..parm) end if ps.type ~= 'boolean' then -- we need a value! This should follow val = arg[i+1] i = i + 1 xassert(val,parm.." was expecting a value") end else -- a parameter parm = parmlist[iparm] if not parm then -- extra unnamed parameters are indexed starting at 1 parm = iextra iextra = iextra + 1 ps = { type = 'string' } else ps = parms[parm] end if not ps.varargs then iparm = iparm + 1 end val = arg[i] end ps.used = true val = convert_parameter(ps,val) set_result(ps,parm,val) if is_filetype(ps.type) then set_result(ps,parm..'_name',arg[i]) end if lapp.callback then lapp.callback(parm,arg[i],res) end i = i + 1 end -- check unused parms, set defaults and check if any required parameters were missed for parm,ps in pairs(parms) do if not ps.used then if ps.required then error("missing required parameter: "..parm) end set_result(ps,parm,ps.defval) end end return res end setmetatable(lapp, { __call = function(tbl,str) return process_options_string(str) end, __index = { open = open, quit = quit, error = error, assert = xassert, } }) return lapp