Skip to content
89 changes: 72 additions & 17 deletions lmod/pythonic/optparse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,18 @@ API

Create command line parser.

opt.add_options{shortflag, longflag, action=action, metavar=metavar, dest=dest, help=help}
opt.add_options{shortflag, longflag, action=action, metavar=metavar, dest=dest, help=help, default=default}

Add command line option specification. This may be called multiple times.
action: 'store'|'store_true'|'store_false'. Default is 'store'
dest: name of returned option table field. Default is the last option-name.
'-' is replaced with '_'
metavar: name used in help text
type: 'int'|'float'|'number'. Default is string

opt.parse_args() --> options, args
opt.parse_args(arglist) --> options, args

Perform argument parsing.
Perform argument parsing. arglist is optional, defaults to global varable 'arg'

DEPENDENCIES

Expand Down Expand Up @@ -85,15 +90,10 @@ LICENSE

--]]

local M = {_TYPE='module', _NAME='pythonic.optparse', _VERSION='0.3.20111128'}
local M = {_TYPE='module', _NAME='pythonic.optparse', _VERSION='0.4.20120827'}

local ipairs = ipairs
local unpack = unpack
local io = io
local table = table
local os = os
local arg = arg

local arg,assert,io,ipairs,math,os,table,tonumber,tostring,type,unpack
= arg,assert,io,ipairs,math,os,table,tonumber,tostring,type,unpack

local function OptionParser(t)
local usage = t.usage
Expand All @@ -108,17 +108,34 @@ local function OptionParser(t)
os.exit(1)
end

local function is_in_list(list,val)
for _,v in ipairs(list) do
if v == val then return true end
end
end

local function luatype(otype)
if (otype == 'int' or otype == 'float' or otype == 'number' ) then return 'number' end
return 'string'
end

function o.add_option(optdesc)
option_descriptions[#option_descriptions+1] = optdesc
for _,v in ipairs(optdesc) do
option_of[v] = optdesc
end
local dest = optdesc.dest or optdesc[#optdesc]:match('^%-+(.*)') -- fallback to last option
optdesc.dest = dest:gsub('-','_')
optdesc.metavar = optdesc.metavar or dest:upper()
if optdesc.default then
assert( type( optdesc.default) == luatype(optdesc.type), 'default type mismatch, option '..optdesc[#optdesc] )
end
end
function o.parse_args()
function o.parse_args(_arg)
-- expand options (e.g. "--input=file" -> "--input", "file")
local arg = {unpack(arg)}
local arg = {unpack(_arg or arg)}
for i=#arg,1,-1 do local v = arg[i]
local flag, val = v:match('^(%-%-%w+)=(.*)')
local flag, val = v:match('^(%-%-[%w_-]+)=(.*)')
if flag then
arg[i] = flag
table.insert(arg, i+1, val)
Expand All @@ -137,6 +154,22 @@ local function OptionParser(t)
i = i + 1
val = arg[i]
if not val then o.fail('option requires an argument ' .. v) end
if luatype(optdesc.type) == 'number' then
local num = tonumber(val)
if num then
if optdesc.type == 'int' and (num ~= math.floor(num)) then
o.fail(('option %s: number %s not an int'):format(v,val))
num = nil
end
else
o.fail(('option %s: %s not a %s'):format(v,val,optdesc.type))
end
val = num
end
if optdesc.choices and not is_in_list(optdesc.choices, val) then
o.fail(('illegal value for option %s: %s'):format(v,val))
val = nil
end
elseif action == 'store_true' then
val = true
elseif action == 'store_false' then
Expand All @@ -157,6 +190,12 @@ local function OptionParser(t)
io.stdout:write(t.version .. "\n")
os.exit()
end
for _,optdesc in ipairs(option_descriptions) do
local name = optdesc.dest
if optdesc.default and (options[name] == nil) then
options[name] = optdesc.default
end
end
return options, args
end

Expand All @@ -166,7 +205,7 @@ local function OptionParser(t)
for _,flag in ipairs(optdesc) do
local sflagend
if action == nil or action == 'store' then
local metavar = optdesc.metavar or optdesc.dest:upper()
local metavar = optdesc.metavar
sflagend = #flag == 2 and ' ' .. metavar
or '=' .. metavar
else
Expand All @@ -178,16 +217,32 @@ local function OptionParser(t)
end

function o.print_help()
io.stdout:write("Usage: " .. usage:gsub('%%prog', arg[0]) .. "\n")
io.stdout:write("Usage: " .. usage:gsub('%%prog', arg[0] or '') .. "\n")
io.stdout:write("\n")
io.stdout:write("Options:\n")
local maxwidth = 0
for _,optdesc in ipairs(option_descriptions) do
maxwidth = math.max(maxwidth, #flags_str(optdesc))
end
for _,optdesc in ipairs(option_descriptions) do
local help = optdesc.help or ''
if optdesc.choices or optdesc.default then
if #help>0 then help = help .. ' ' end
help = help .. '('
if optdesc.choices then
for i,v in ipairs(optdesc.choices) do
if i>1 then help = help.. '|' end
help = help..tostring(v)
end
end
if optdesc.choices and optdesc.default then help = help..' ; ' end
if optdesc.default then
help = help .. 'default: '.. tostring(optdesc.default)
end
help = help .. ')'
end
io.stdout:write(" " .. ('%-'..maxwidth..'s '):format(flags_str(optdesc))
.. optdesc.help .. "\n")
.. help .. "\n")
end
end
if t.add_help_option == nil or t.add_help_option == true then
Expand Down
57 changes: 56 additions & 1 deletion test/test.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,62 @@
package.path = './lmod/?.lua;'..package.path

arg = { '-d', '-l', 'DEBUG' , 'localhost', '60000', [0] = 'testdeamon'} -- set global arg

local OP = require 'pythonic.optparse'

-- TODO: add tests
local opt = OP.OptionParser{usage="%prog [options] address port \n", version='0.0', add_help_option=false}
opt.add_option{'-h', '--help', help = 'show this help message and exit', action='store_true' }
opt.add_option{'-d', '--daemon', help = 'Run as daemon', action='store_true' }
opt.add_option{'-l', '--log-level', choices = {'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'}, default = 'INFO', metavar='LEVEL' }
opt.add_option{ '--logfile', help = 'Name of logfile', metavar='FILE' }
opt.add_option{'-c', '--config', help = 'Name of configfile', default = '/etc/opt/test', metavar='FILE' }
opt.add_option{'-s', '--check-interval',help = 'Seconds between checking config', type='int', dest='ci', default = 15 , metavar='SEC'}
opt.add_option{ '--check-float', help = 'Check floating raft', type='float', metavar='liters'}
--opt.print_help()

local options, args = opt.parse_args() -- use global arg

assert(options.log_level=='DEBUG')
assert(options.config=='/etc/opt/test') -- default value
assert(options.daemon==true) -- -d
assert(options.logfile == nil)
assert(options.ci == 15) -- dest, default
assert(options.check_float == nil)
assert(#args == 2)
assert(args[1] == 'localhost')
assert(args[2] == '60000')

-- Next case, now passing arg as paramenter
options, args = opt.parse_args{ 'localhost', '60001', '-l', 'FATAL', '-s', '60' ,
'--config', '/home/test/config', '--logfile', '/var/log/test', '--check-float=3.14' }
assert(options.log_level=='FATAL')
assert(options.config=='/home/test/config') -- config
assert(options.logfile == '/var/log/test')
assert(options.daemon==nil)
assert(options.ci == 60) -- dest, default
assert(options.check_float == 3.14) -- float and --name=var syntax
assert(#args == 2)
assert(args[1] == 'localhost')
assert(args[2] == '60001')


-- harness for error cases:
opt.fail=error
local function check_errorcase(args, errtext)
local res, err = pcall(opt.parse_args, args)
assert(res==false, 'Illegal parameter not caught')
if errtext then
assert(err:find(errtext, nil, true), 'Unexpected error text')
else
print("'"..err.."'")
end
end

-- test error cases:
check_errorcase({ '--bogous', 'foo'}, 'invalid option --bogous')
check_errorcase({ '-l' }, 'option requires an argument -l')
check_errorcase({ '-l', 'DEADLY' }, 'illegal value for option -l: DEADLY')
check_errorcase({ '-s', '6f' }, 'option -s: 6f not a int')
check_errorcase({ '-s', '3.14' }, 'option -s: number 3.14 not an int')

print 'DONE'