]> git.lizzy.rs Git - metalua.git/blob - src/lib/metalua/clopts.mlua
Merge remote branch 'origin/master'
[metalua.git] / src / lib / metalua / clopts.mlua
1 --------------------------------------------------------------------------------
2 -- Command Line OPTionS handler
3 -- ============================
4 --
5 -- This lib generates parsers for command-line options. It encourages
6 -- the following of some common idioms: I'm pissed off by Unix tools
7 -- which sometimes will let you concatenate single letters options,
8 -- sometimes won't, will prefix long name options with simple dashes
9 -- instead of doubles, etc.
10 --
11 --------------------------------------------------------------------------------
12
13 -- TODO:
14 -- * add a generic way to unparse options ('grab everything')
15 -- * doc
16 -- * when a short options that takes a param isn't the last element of a series
17 --   of shorts, take the remaining of the sequence as that param, e.g. -Ifoo
18 -- * let unset strings/numbers with +
19 -- * add a ++ long counterpart to +
20 --
21
22 -{ extension 'match' }
23
24 function clopts(cfg)
25    local short, long, param_func = { }, { }
26    local legal_types = table.transpose{ 
27       'boolean','string','number','string*','number*','nil', '*' }
28
29    -----------------------------------------------------------------------------
30    -- Fill short and long name indexes, and check its validity
31    -----------------------------------------------------------------------------
32    for x in ivalues(cfg) do
33       local xtype = type(x)
34       if xtype=='table' then
35          if not x.type then x.type='nil' end
36          if not legal_types[x.type] then error ("Invalid type name "..x.type) end
37          if x.short then
38             if short[x.short] then error ("multiple definitions for option "..x.short) 
39             else short[x.short] = x end
40          end
41          if x.long then
42             if long[x.long] then error ("multiple definitions for option "..x.long) 
43             else long[x.long] = x end
44          end
45       elseif xtype=='function' then
46          if param_func then error "multiple parameters handler in clopts"
47          else param_func=x end
48       end
49    end
50
51    -----------------------------------------------------------------------------
52    -- Print a help message, summarizing how to use the command line
53    -----------------------------------------------------------------------------
54    local function print_usage(msg)
55       if msg then print(msg,'\n') end
56       print(cfg.usage or "Options:\n")
57       for x in values(cfg) do
58          if type(x) == 'table' then
59             local opts = { }
60             if x.type=='boolean' then 
61                if x.short then opts = { '-'..x.short..'/+'..x.short } end
62                if x.long  then table.insert (opts, '--'..x.long..'/++'..x.long) end
63             else
64                if x.short then opts = { '-'..x.short..' <'..x.type..'>' } end
65                if x.long  then table.insert (opts,  '--'..x.long..' <'..x.type..'>' ) end
66             end
67             printf("  %s: %s", table.concat(opts,', '), x.usage or '<undocumented>')
68          end
69       end
70       print''
71    end
72
73    -- Unless overridden, -h and --help display the help msg
74    local default_help = { action = | | print_usage() or os.exit(0);
75                           long='help';short='h';type='nil'}
76    if not short.h   then short.h   = default_help end
77    if not long.help then long.help = default_help end
78
79    -----------------------------------------------------------------------------
80    -- Helper function for options parsing. Execute the attached action and/or
81    -- register the config in cfg.
82    --
83    -- * cfg  is the table which registers the options
84    -- * dict the name->config entry hash table that describes options
85    -- * flag is the prefix '-', '--' or '+'
86    -- * opt  is the option name
87    -- * i    the current index in the arguments list
88    -- * args is the arguments list
89    -----------------------------------------------------------------------------
90    local function actionate(cfg, dict, flag, opt, i, args)
91       local entry = dict[opt]
92       if not entry then print_usage ("invalid option "..flag..opt); return false; end
93       local etype, name = entry.type, entry.name or entry.long or entry.short
94       match etype with
95       | 'string' | 'number' | 'string*' | 'number*' -> 
96          if flag=='+' or flag=='++' then 
97             print_usage ("flag "..flag.." is reserved for boolean options, not for "..opt)
98             return false
99          end
100          local arg = args[i+1]
101          if not arg then 
102             print_usage ("missing parameter for option "..flag..opt)
103             return false
104          end
105          if etype:strmatch '^number' then 
106             arg = tonumber(arg)
107             if not arg then 
108                print_usage ("option "..flag..opt.." expects a number argument")
109             end
110          end
111          if etype:strmatch '%*$' then 
112             if not cfg[name] then cfg[name]={ } end
113             table.insert(cfg[name], arg)
114          else cfg[name] = arg end
115          if entry.action then entry.action(arg) end
116          return i+2
117       | 'boolean' -> 
118          local arg = flag=='-' or flag=='--'
119          cfg[name] = arg
120          if entry.action then entry.action(arg) end
121          return i+1
122       | 'nil' -> 
123          cfg[name] = true;
124          if entry.action then entry.action() end
125          return i+1
126       | '*' -> 
127          local arg = table.isub(args, i+1, #args)
128          cfg[name] = arg
129          if entry.action then entry.action(arg) end
130          return #args+1
131       |  _ -> assert( false, 'undetected bad type for clopts action')
132       end
133    end
134
135    -----------------------------------------------------------------------------
136    -- Parse a list of commands: the resulting function
137    -----------------------------------------------------------------------------
138    local function parse(...)
139       local cfg = { }
140       if not ... then return cfg end
141       local args = type(...)=='table' and ... or {...}
142       local i, i_max = 1, #args
143       while i <= i_max do         
144          local arg, flag, opt, opts = args[i]
145          --printf('beginning of loop: i=%i/%i, arg=%q', i, i_max, arg)
146          if arg=='-' then
147             i=actionate (cfg, short, '-', '', i, args)
148             -{ `Goto 'continue' }
149          end
150
151          -----------------------------------------------------------------------
152          -- double dash option
153          -----------------------------------------------------------------------
154          flag, opt = arg:strmatch "^(%-%-)(.*)"
155          if opt then
156             i=actionate (cfg, long, flag, opt, i, args)
157             -{ `Goto 'continue' }
158          end
159
160          -----------------------------------------------------------------------
161          -- double plus option
162          -----------------------------------------------------------------------
163          flag, opt = arg:strmatch "^(%+%+)(.*)"
164          if opt then
165             i=actionate (cfg, long, flag, opt, i, args)
166             -{ `Goto 'continue' }
167          end
168
169          -----------------------------------------------------------------------
170          -- single plus or single dash series of short options
171          -----------------------------------------------------------------------
172          flag, opts = arg:strmatch "^([+-])(.+)"
173          if opts then 
174             local j_max, i2 = opts:len()
175             for j = 1, j_max do
176                opt = opts:sub(j,j)
177                --printf ('parsing short opt %q', opt)               
178                i2 = actionate (cfg, short, flag, opt, i, args)
179                if i2 ~= i+1 and j < j_max then 
180                   error ('short option '..opt..' needs a param of type '..short[opt])
181                end               
182             end
183             i=i2 
184             -{ `Goto 'continue' }
185          end
186
187          -----------------------------------------------------------------------
188          -- handler for non-option parameter
189          -----------------------------------------------------------------------         
190          if param_func then param_func(args[i]) end
191          if cfg.params then table.insert(cfg.params, args[i])
192          else cfg.params = { args[i] } end
193          i=i+1
194
195          -{ `Label 'continue' }
196          if not i then return false end
197       end -- </while>
198       return cfg
199    end
200
201    return parse
202 end
203
204