removed useless file
[qmc.git] / lapp.lua
blobcb7420e1944958113e7e09e7eec557e1da37d975
1 -- lapp.lua
2 -- Simple command-line parsing using human-readable specification
3 -----------------------------
4 --~ -- args.lua
5 --~ local args = require ('lapp') [[
6 --~ Testing parameter handling
7 --~ -p Plain flag (defaults to false)
8 --~ -q,--quiet Plain flag with GNU-style optional long name
9 --~ -o (string) Required string option
10 --~ -n (number) Required number option
11 --~ -s (default 1.0) Option that takes a number, but will default
12 --~ <start> (number) Required number argument
13 --~ <input> (default stdin) A parameter which is an input file
14 --~ <output> (default stdout) One that is an output file
15 --~ ]]
16 --~ for k,v in pairs(args) do
17 --~ print(k,v)
18 --~ end
19 -------------------------------
20 --~ > args -pq -o help -n 2 2.3
21 --~ input file (781C1B78)
22 --~ p true
23 --~ s 1
24 --~ output file (781C1B98)
25 --~ quiet true
26 --~ start 2.3
27 --~ o help
28 --~ n 2
29 --------------------------------
31 lapp = {}
33 local append = table.insert
34 local usage
35 local open_files = {}
36 local parms = {}
37 local aliases = {}
38 local parmlist = {}
40 local filetypes = {
41 stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},
42 stderr = {io.stderr,'file-out'}
45 local function quit(msg,no_usage)
46 if msg then
47 io.stderr:write(msg..'\n\n')
48 end
49 if not no_usage then
50 io.stderr:write(usage)
51 end
52 os.exit(1);
53 end
55 local function error(msg,no_usage)
56 quit(arg[0]:gsub('.+[\\/]','')..':'..msg,no_usage)
57 end
59 local function ltrim(line)
60 return line:gsub('^%s*','')
61 end
63 local function rtrim(line)
64 return line:gsub('%s*$','')
65 end
67 local function trim(s)
68 return ltrim(rtrim(s))
69 end
71 local function open (file,opt)
72 local val,err = io.open(file,opt)
73 if not val then error(err,true) end
74 append(open_files,val)
75 return val
76 end
78 local function xassert(condn,msg)
79 if not condn then
80 error(msg)
81 end
82 end
84 local function range_check(x,min,max,parm)
85 xassert(min <= x and max >= x,parm..' out of range')
86 end
88 local function xtonumber(s)
89 local val = tonumber(s)
90 if not val then error("unable to convert to number: "..s) end
91 return val
92 end
94 local function is_filetype(type)
95 return type == 'file-in' or type == 'file-out'
96 end
98 local types = {}
100 local function convert_parameter(ps,val)
101 if ps.converter then
102 val = ps.converter(val)
104 if ps.type == 'number' then
105 val = xtonumber(val)
106 elseif is_filetype(ps.type) then
107 val = open(val,(ps.type == 'file-in' and 'r') or 'w' )
108 elseif ps.type == 'boolean' then
109 val = true
111 if ps.constraint then
112 ps.constraint(val)
114 return val
117 function lapp.add_type (name,converter,constraint)
118 types[name] = {converter=converter,constraint=constraint}
121 local function force_short(short)
122 xassert(#short==1,short..": short parameters should be one character")
125 function process_options_string(str)
126 local res = {}
127 local varargs
129 local function check_varargs(s)
130 local res,cnt = s:gsub('%.%.%.$','')
131 varargs = cnt > 0
132 return res
135 local function set_result(ps,parm,val)
136 if not ps.varargs then
137 res[parm] = val
138 else
139 if not res[parm] then
140 res[parm] = { val }
141 else
142 append(res[parm],val)
147 usage = str
149 for line in str:gfind('([^\n]*)\n') do
150 local optspec,optparm,i1,i2,defval,vtype,constraint
151 line = ltrim(line)
152 -- flags: either -<short> or -<short>,<long>
153 i1,i2,optspec = line:find('^%-(%S+)')
154 if i1 then
155 optspec = check_varargs(optspec)
156 local short,long = optspec:match('([^,]+),(.+)')
157 if short then
158 optparm = long:sub(3)
159 aliases[short] = optparm
160 force_short(short)
161 else
162 optparm = optspec
163 force_short(optparm)
165 else -- is it <parameter_name>?
166 i1,i2,optparm = line:find('(%b<>)')
167 if i1 then
168 -- so <input file...> becomes input_file ...
169 optparm = check_varargs(optparm:sub(2,-2)):gsub('%A','_')
170 append(parmlist,optparm)
173 if i1 then -- this is not a pure doc line
174 local last_i2 = i2
175 local sval
176 line = ltrim(line:sub(i2+1))
177 -- do we have (default <val>) or (<type>)?
178 i1,i2,typespec = line:find('^%s*(%b())')
179 if i1 then
180 typespec = trim(typespec:sub(2,-2)) -- trim the parens and any space
181 sval = typespec:match('default%s+(.+)')
182 if sval then
183 local val = tonumber(sval)
184 if val then -- we have a number!
185 defval = val
186 vtype = 'number'
187 elseif filetypes[sval] then
188 local ft = filetypes[sval]
189 defval = ft[1]
190 vtype = ft[2]
191 else
192 defval = sval
193 vtype = 'string'
195 else
196 local min,max = typespec:match '([^%.]+)%.%.(.+)'
197 if min then -- it's (min..max)
198 vtype = 'number'
199 min = xtonumber(min)
200 max = xtonumber(max)
201 constraint = function(x)
202 range_check(x,min,max,optparm)
204 else -- () just contains type of required parameter
205 vtype = typespec
208 else -- must be a plain flag, no extra parameter required
209 defval = false
210 vtype = 'boolean'
212 local ps = {
213 type = vtype,
214 defval = defval,
215 required = defval == nil,
216 comment = line:sub((i2 or last_i2)+1) or optparm,
217 constraint = constraint,
218 varargs = varargs
220 if types[vtype] then
221 local converter = types[vtype].converter
222 if type(converter) == 'string' then
223 ps.type = converter
224 else
225 ps.converter = converter
227 ps.constraint = types[vtype].constraint
229 parms[optparm] = ps
232 -- cool, we have our parms, let's parse the command line args
233 local iparm = 1
234 local iextra = 1
235 local i = 1
236 local parm,ps,val
237 while i <= #arg do
238 -- look for a flag, -<short flags> or --<long flag>
239 local i1,i2,dash,parmstr = arg[i]:find('^(%-+)(%a.*)')
240 if i1 then -- we have a flag
241 if #dash == 2 then -- long option
242 parm = parmstr
243 else -- short option
244 if #parmstr == 1 then
245 parm = parmstr
246 else -- multiple flags after a '-',?
247 parm = parmstr:sub(1,1)
248 if parmstr:find('^%a%d+') then
249 -- a short option followed by a digit? (exception for AW ;))
250 -- push ahead into the arg array
251 table.insert(arg,i+1,parmstr:sub(2))
252 else
253 -- push multiple flags into the arg array!
254 for k = 2,#parmstr do
255 table.insert(arg,i+k-1,'-'..parmstr:sub(k,k))
260 if parm == 'h' or parm == 'help' then
261 quit()
263 if aliases[parm] then parm = aliases[parm] end
264 ps = parms[parm]
265 if not ps then error("unrecognized parameter: "..parm) end
266 if ps.type ~= 'boolean' then -- we need a value! This should follow
267 val = arg[i+1]
268 i = i + 1
269 xassert(val,parm.." was expecting a value")
271 else -- a parameter
272 parm = parmlist[iparm]
273 if not parm then
274 -- extra unnamed parameters are indexed starting at 1
275 parm = iextra
276 iextra = iextra + 1
277 ps = { type = 'string' }
278 else
279 ps = parms[parm]
281 if not ps.varargs then
282 iparm = iparm + 1
284 val = arg[i]
286 ps.used = true
287 val = convert_parameter(ps,val)
288 set_result(ps,parm,val)
289 if is_filetype(ps.type) then
290 set_result(ps,parm..'_name',arg[i])
292 if lapp.callback then
293 lapp.callback(parm,arg[i],res)
295 i = i + 1
297 -- check unused parms, set defaults and check if any required parameters were missed
298 for parm,ps in pairs(parms) do
299 if not ps.used then
300 if ps.required then error("missing required parameter: "..parm) end
301 set_result(ps,parm,ps.defval)
304 return res
307 setmetatable(lapp, {
308 __call = function(tbl,str) return process_options_string(str) end,
309 __index = {
310 open = open,
311 quit = quit,
312 error = error,
313 assert = xassert,
317 return lapp