diff options
Diffstat (limited to 'Data/Libraries/Penlight/lua/pl/lapp.lua')
-rw-r--r-- | Data/Libraries/Penlight/lua/pl/lapp.lua | 451 |
1 files changed, 451 insertions, 0 deletions
diff --git a/Data/Libraries/Penlight/lua/pl/lapp.lua b/Data/Libraries/Penlight/lua/pl/lapp.lua new file mode 100644 index 0000000..56d1d9d --- /dev/null +++ b/Data/Libraries/Penlight/lua/pl/lapp.lua @@ -0,0 +1,451 @@ +--- Simple command-line parsing using human-readable specification. +-- Supports GNU-style parameters. +-- +-- lapp = require 'pl.lapp' +-- local args = lapp [[ +-- Does some calculations +-- -o,--offset (default 0.0) Offset to add to scaled number +-- -s,--scale (number) Scaling factor +-- <number> (number) Number to be scaled +-- ]] +-- +-- print(args.offset + args.scale * args.number) +-- +-- Lines beginning with `'-'` are flags; there may be a short and a long name; +-- lines beginning with `'<var>'` are arguments. Anything in parens after +-- the flag/argument is either a default, a type name or a range constraint. +-- +-- See @{08-additional.md.Command_line_Programs_with_Lapp|the Guide} +-- +-- Dependencies: `pl.sip` +-- @module pl.lapp + +local status,sip = pcall(require,'pl.sip') +if not status then + sip = require 'sip' +end +local match = sip.match_at_start +local append,tinsert = table.insert,table.insert + +sip.custom_pattern('X','(%a[%w_%-]*)') + +local function lines(s) return s:gmatch('([^\n]*)\n') end +local function lstrip(str) return str:gsub('^%s+','') end +local function strip(str) return lstrip(str):gsub('%s+$','') end +local function at(s,k) return s:sub(k,k) end + +local lapp = {} + +local open_files,parms,aliases,parmlist,usage,script + +lapp.callback = false -- keep Strict happy + +local filetypes = { + stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'}, + stderr = {io.stderr,'file-out'} +} + +--- controls whether to dump usage on error. +-- Defaults to true +lapp.show_usage_error = true + +--- quit this script immediately. +-- @string msg optional message +-- @bool no_usage suppress 'usage' display +function lapp.quit(msg,no_usage) + if no_usage == 'throw' then + error(msg) + end + if msg then + io.stderr:write(msg..'\n\n') + end + if not no_usage then + io.stderr:write(usage) + end + os.exit(1) +end + +--- print an error to stderr and quit. +-- @string msg a message +-- @bool no_usage suppress 'usage' display +function lapp.error(msg,no_usage) + if not lapp.show_usage_error then + no_usage = true + elseif lapp.show_usage_error == 'throw' then + no_usage = 'throw' + end + lapp.quit(script..': '..msg,no_usage) +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 lapp.open (file,opt) + local val,err = io.open(file,opt) + if not val then lapp.error(err,true) end + append(open_files,val) + return val +end + +--- quit if the condition is false. +-- @bool condn a condition +-- @string msg message text +function lapp.assert(condn,msg) + if not condn then + lapp.error(msg) + end +end + +local function range_check(x,min,max,parm) + lapp.assert(min <= x and max >= x,parm..' out of range') +end + +local function xtonumber(s) + local val = tonumber(s) + if not val then lapp.error("unable to convert to number: "..s) end + return val +end + +local types = {} + +local builtin_types = {string=true,number=true,['file-in']='file',['file-out']='file',boolean=true} + +local function convert_parameter(ps,val) + if ps.converter then + val = ps.converter(val) + end + if ps.type == 'number' then + val = xtonumber(val) + elseif builtin_types[ps.type] == 'file' then + val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' ) + elseif ps.type == 'boolean' then + return val + end + if ps.constraint then + ps.constraint(val) + end + return val +end + +--- add a new type to Lapp. These appear in parens after the value like +-- a range constraint, e.g. '<ival> (integer) Process PID' +-- @string name name of type +-- @param converter either a function to convert values, or a Lua type name. +-- @func[opt] constraint optional function to verify values, should use lapp.error +-- if failed. +function lapp.add_type (name,converter,constraint) + types[name] = {converter=converter,constraint=constraint} +end + +local function force_short(short) + lapp.assert(#short==1,short..": short parameters should be one character") +end + +-- deducing type of variable from default value; +local function process_default (sval,vtype) + local val, success + if not vtype or vtype == 'number' then + val = tonumber(sval) + end + if val then -- we have a number! + return val,'number' + elseif filetypes[sval] then + local ft = filetypes[sval] + return ft[1],ft[2] + else + if sval == 'true' and not vtype then + return true, 'boolean' + end + if sval:match '^["\']' then sval = sval:sub(2,-2) end + + local ps = types[vtype] or {} + ps.type = vtype + + local show_usage_error = lapp.show_usage_error + lapp.show_usage_error = "throw" + success, val = pcall(convert_parameter, ps, sval) + lapp.show_usage_error = show_usage_error + if success then + return val, vtype or 'string' + end + + return sval,vtype or 'string' + end +end + +--- process a Lapp options string. +-- Usually called as `lapp()`. +-- @string str the options text +-- @tparam {string} args a table of arguments (default is `_G.arg`) +-- @return a table with parameter-value pairs +function lapp.process_options_string(str,args) + local results = {} + local varargs + local arg = args or _G.arg + open_files = {} + parms = {} + aliases = {} + parmlist = {} + + local function check_varargs(s) + local res,cnt = s:gsub('^%.%.%.%s*','') + return res, (cnt > 0) + end + + local function set_result(ps,parm,val) + parm = type(parm) == "string" and parm:gsub("%W", "_") or parm -- so foo-bar becomes foo_bar in Lua + if not ps.varargs then + results[parm] = val + else + if not results[parm] then + results[parm] = { val } + else + append(results[parm],val) + end + end + end + + usage = str + + for _,a in ipairs(arg) do + if a == "-h" or a == "--help" then + return lapp.quit() + end + end + + + for line in lines(str) do + local res = {} + local optparm,defval,vtype,constraint,rest + line = lstrip(line) + local function check(str) + return match(str,line,res) + end + + -- flags: either '-<short>', '-<short>,--<long>' or '--<long>' + if check '-$v{short}, --$o{long} $' or check '-$v{short} $' or check '--$o{long} $' then + if res.long then + optparm = res.long:gsub('[^%w%-]','_') -- I'm not sure the $o pattern will let anything else through? + if #res.rest == 1 then optparm = optparm .. res.rest end + if res.short then aliases[res.short] = optparm end + else + optparm = res.short + end + if res.short and not lapp.slack then force_short(res.short) end + res.rest, varargs = check_varargs(res.rest) + elseif check '$<{name} $' then -- is it <parameter_name>? + -- so <input file...> becomes input_file ... + optparm,rest = res.name:match '([^%.]+)(.*)' + optparm = optparm:gsub('%A','_') + varargs = rest == '...' + append(parmlist,optparm) + end + -- this is not a pure doc line and specifies the flag/parameter type + if res.rest then + line = res.rest + res = {} + local optional + -- do we have ([optional] [<type>] [default <val>])? + if match('$({def} $',line,res) or match('$({def}',line,res) then + local typespec = strip(res.def) + local ftype, rest = typespec:match('^(%S+)(.*)$') + rest = strip(rest) + if ftype == 'optional' then + ftype, rest = rest:match('^(%S+)(.*)$') + rest = strip(rest) + optional = true + end + local default + if ftype == 'default' then + default = true + if rest == '' then lapp.error("value must follow default") end + else -- a type specification + if match('$f{min}..$f{max}',ftype,res) then + -- a numerical range like 1..10 + local min,max = res.min,res.max + vtype = 'number' + constraint = function(x) + range_check(x,min,max,optparm) + end + elseif not ftype:match '|' then -- plain type + vtype = ftype + else + -- 'enum' type is a string which must belong to + -- one of several distinct values + local enums = ftype + local enump = '|' .. enums .. '|' + vtype = 'string' + constraint = function(s) + lapp.assert(enump:match('|'..s..'|'), + "value '"..s.."' not in "..enums + ) + end + end + end + res.rest = rest + typespec = res.rest + -- optional 'default value' clause. Type is inferred as + -- 'string' or 'number' if there's no explicit type + if default or match('default $r{rest}',typespec,res) then + defval,vtype = process_default(res.rest,vtype) + 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 and not optional, + comment = res.rest or optparm, + constraint = constraint, + varargs = varargs + } + varargs = nil + 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 + elseif not builtin_types[vtype] and vtype then + lapp.error(vtype.." is unknown type") + 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 + local end_of_flags = false + + local function check_parm (parm) + local eqi = parm:find '[=:]' + if eqi then + tinsert(arg,i+1,parm:sub(eqi+1)) + parm = parm:sub(1,eqi-1) + end + return parm,eqi + end + + local function is_flag (parm) + return parms[aliases[parm] or parm] + end + + while i <= #arg do + local theArg = arg[i] + local res = {} + -- after '--' we don't parse args and they end up in + -- the array part of the result (args[1] etc) + if theArg == '--' then + end_of_flags = true + iparm = #parmlist + 1 + i = i + 1 + theArg = arg[i] + if not theArg then + break + end + end + -- look for a flag, -<short flags> or --<long flag> + if not end_of_flags and (match('--$S{long}',theArg,res) or match('-$S{short}',theArg,res)) then + if res.long then -- long option + parm = check_parm(res.long) + elseif #res.short == 1 or is_flag(res.short) then + parm = res.short + else + local parmstr,eq = check_parm(res.short) + if not eq then + parm = at(parmstr,1) + local flag = is_flag(parm) + if flag and flag.type ~= 'boolean' then + --if isdigit(at(parmstr,2)) then + -- a short option followed by a digit is an exception (for AW;)) + -- push ahead into the arg array + tinsert(arg,i+1,parmstr:sub(2)) + else + -- push multiple flags into the arg array! + for k = 2,#parmstr do + tinsert(arg,i+k-1,'-'..at(parmstr,k)) + end + end + else + parm = parmstr + end + end + if aliases[parm] then parm = aliases[parm] end + if not parms[parm] and (parm == 'h' or parm == 'help') then + lapp.quit() + end + else -- a parameter + parm = parmlist[iparm] + if not parm then + -- extra unnamed parameters are indexed starting at 1 + parm = iextra + ps = { type = 'string' } + parms[parm] = ps + iextra = iextra + 1 + else + ps = parms[parm] + end + if not ps.varargs then + iparm = iparm + 1 + end + val = theArg + end + ps = parms[parm] + if not ps then lapp.error("unrecognized parameter: "..parm) end + if ps.type ~= 'boolean' then -- we need a value! This should follow + if not val then + i = i + 1 + val = arg[i] + theArg = val + end + lapp.assert(val,parm.." was expecting a value") + else -- toggle boolean flags (usually false -> true) + val = not ps.defval + end + ps.used = true + val = convert_parameter(ps,val) + set_result(ps,parm,val) + if builtin_types[ps.type] == 'file' then + set_result(ps,parm..'_name',theArg) + end + if lapp.callback then + lapp.callback(parm,theArg,res) + end + i = i + 1 + val = nil + 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 lapp.error("missing required parameter: "..parm) end + set_result(ps,parm,ps.defval) + end + end + return results +end + +if arg then + script = arg[0] + script = script or rawget(_G,"LAPP_SCRIPT") or "unknown" + -- strip dir and extension to get current script name + script = script:gsub('.+[\\/]',''):gsub('%.%a+$','') +else + script = "inter" +end + + +setmetatable(lapp, { + __call = function(tbl,str,args) return lapp.process_options_string(str,args) end, +}) + + +return lapp + + |