1-- Copyright (c) 2018-2021, OARC, Inc.
2-- All rights reserved.
3--
4-- This file is part of dnsjit.
5--
6-- dnsjit is free software: you can redistribute it and/or modify
7-- it under the terms of the GNU General Public License as published by
8-- the Free Software Foundation, either version 3 of the License, or
9-- (at your option) any later version.
10--
11-- dnsjit is distributed in the hope that it will be useful,
12-- but WITHOUT ANY WARRANTY; without even the implied warranty of
13-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14-- GNU General Public License for more details.
15--
16-- You should have received a copy of the GNU General Public License
17-- along with dnsjit.  If not, see <http://www.gnu.org/licenses/>.
18
19-- dnsjit.lib.getopt
20-- Parse and handle arguments
21--   local getopt = require("dnsjit.lib.getopt").new({
22--       { "v", "verbose", 0, "Enable verbosity", "?+" },
23--       { nil, "host", "localhost", "Set host", "?" },
24--       { "p", nil, 53, "Set port", "?" },
25--   })
26-- .
27--   local left = getopt:parse()
28-- .
29--   print("host", getopt:val("host"))
30--   print("port", getopt:val("p"))
31--
32-- A "getopt long" implementation to easily handle command line arguments
33-- and display usage.
34-- An option is the short name (one character), long name,
35-- default value (which also defines the type), help text and extensions.
36-- Options are by default required, see extensions to change this.
37-- .LP
38-- The Lua types allowed are
39-- .BR boolean ,
40-- .BR string ,
41-- .BR number .
42-- .LP
43-- The extensions available are:
44-- .TP
45-- .B ?
46-- Make the option optional.
47-- .TP
48-- .B *
49-- For string and number options this make it possible to specified it
50-- multiple times and all values will be returned in a table.
51-- .TP
52-- .B +
53-- For number options this will act as an counter increaser, the value will
54-- be the default value + 1 for each time the option is given.
55-- .LP
56-- Option
57-- .I -h
58-- and
59-- .I --help
60-- are automatically added if the option
61-- .I --help
62-- is not already defined.
63-- .SS Attributes
64-- .TP
65-- left
66-- A table that contains the arguments left after parsing, same as returned by
67-- .IR parse() .
68-- .TP
69-- usage_desc
70-- A string that describes the usage of the program, if not set then the
71-- default will be "
72-- .I "program [options...]"
73-- ".
74module(...,package.seeall)
75
76local log = require("dnsjit.core.log")
77
78local module_log = log.new("lib.getopt")
79Getopt = {}
80
81-- Create a new Getopt object.
82-- .I args
83-- is a table with tables that specifies the options available.
84-- Each entry is unpacked and sent to
85-- .BR Getopt:add() .
86function Getopt.new(args)
87    local self = setmetatable({
88        left = {},
89        usage_desc = nil,
90        _opt = {},
91        _s2l = {},
92        _log = log.new("lib.getopt", module_log),
93    }, { __index = Getopt })
94
95    self._log:debug("new()")
96
97    for k, v in pairs(args) do
98        local short, long, default, help, extensions = unpack(v)
99        self:add(short, long, default, help, extensions)
100    end
101
102    return self
103end
104
105-- Return the Log object to control logging of this instance or module.
106function Getopt:log()
107    if self == nil then
108        return module_log
109    end
110    return self._log
111end
112
113-- Add an option.
114function Getopt:add(short, long, default, help, extensions)
115    local optional = false
116    local multiple = false
117    local counter = false
118    local name = long or short
119
120    if type(name) ~= "string" then
121        error("long|short) need to be a string")
122    elseif name == "" then
123        error("name (long|short) needs to be set")
124    end
125    if short and (type(short) ~= "string" or #short ~= 1) then
126        error("short needs to be a string of length 1")
127    end
128
129    if self._opt[name] then
130        error("option "..name.." alredy exists")
131    elseif short and self._s2l[short] then
132        error("option "..short.." alredy exists")
133    end
134
135    local t = type(default)
136    if t ~= "string" and t ~= "number" and t ~= "boolean" then
137        error("option "..name..": invalid type "..t)
138    end
139
140    if type(extensions) == "string" then
141        local n
142        for n = 1, extensions:len() do
143            local extension = extensions:sub(n, n)
144            if extension == "?" then
145                optional = true
146            elseif extension == "*" then
147                multiple = true
148            elseif extension == "+" then
149                counter = true
150            else
151                error("option "..name..": invalid extension "..extension)
152            end
153        end
154    end
155
156    self._opt[name] = {
157        value = nil,
158        short = short,
159        long = long,
160        type = t,
161        default = default,
162        help = help,
163        optional = optional,
164        multiple = multiple,
165        counter = counter,
166    }
167    if long and short then
168        self._s2l[short] = long
169    elseif short and not long then
170        self._s2l[short] = short
171    end
172
173    if not self._opt["help"] then
174        self._opt["help"] = {
175            short = nil,
176            long = "help",
177            type = "boolean",
178            default = false,
179            help = "Display this help text",
180            optional = true,
181        }
182        if not self._s2l["h"] then
183            self._opt["help"].short = "h"
184            self._s2l["h"] = "help"
185        end
186    end
187end
188
189-- Print the usage.
190function Getopt:usage()
191    if self.usage_desc then
192        print("usage: " .. self.usage_desc)
193    else
194        print("usage: program [options...]")
195    end
196
197    local opts = {}
198    for k, _ in pairs(self._opt) do
199        if k ~= "help" then
200            table.insert(opts, k)
201        end
202    end
203    table.sort(opts)
204    table.insert(opts, "help")
205
206    for _, k in pairs(opts) do
207        local v = self._opt[k]
208        local arg
209        if v.type == "string" then
210            arg = " \""..v.default.."\""
211        elseif v.type == "number" and v.counter == false then
212            arg = " "..v.default
213        else
214            arg = ""
215        end
216        if v.long then
217            print("", (v.short and "-"..v.short or "  ").." --"..v.long..arg, v.help)
218        else
219            print("", "-"..v.short..arg, v.help)
220        end
221    end
222end
223
224-- Parse the options.
225-- If
226-- .I args
227-- is not specified or nil then the global
228-- .B arg
229-- is used.
230-- If
231-- .I startn
232-- is given, it will start parsing arguments in the table from that position.
233-- The default position to start at is 2 for
234-- .IR dnsjit ,
235-- see
236-- .BR dnsjit.core (3).
237function Getopt:parse(args, startn)
238    if not args then
239        args = arg
240    end
241
242    local n
243    local opt = nil
244    local left = {}
245    local need_arg = false
246    local stop = false
247    local name
248    for n = startn or 2, table.maxn(args) do
249        if need_arg then
250            if opt.multiple then
251                if opt.value == nil then
252                    opt.value = {}
253                end
254                if opt.type == "number" then
255                    table.insert(opt.value, tonumber(args[n]))
256                else
257                    table.insert(opt.value, args[n])
258                end
259            else
260                if opt.type == "number" then
261                    opt.value = tonumber(args[n])
262                else
263                    opt.value = args[n]
264                end
265            end
266            need_arg = false
267        elseif stop or args[n] == "-" then
268            table.insert(left, args[n])
269        elseif args[n] == "--" then
270            stop = true
271        elseif args[n]:sub(1, 1) == "-" then
272            if args[n]:sub(1, 2) == "--" then
273                name = args[n]:sub(3)
274            else
275                name = args[n]:sub(2)
276                if name:len() > 1 then
277                    local n2, name2
278                    for n2 = 1, name:len() - 1 do
279                        name2 = name:sub(n2, n2)
280                        opt = self._opt[self._s2l[name2]]
281                        if not opt then
282                            error("unknown option "..name2)
283                        end
284                        if opt.type == "number" and opt.counter then
285                            if opt.value == nil then
286                                opt.value = opt.default
287                            end
288                            opt.value = opt.value + 1
289                        elseif opt.type == "boolean" then
290                            if opt.value == nil then
291                                opt.value = opt.default
292                            end
293                            if opt.value then
294                                opt.value = false
295                            else
296                                opt.value = true
297                            end
298                        else
299                            error("invalid short option '"..name2.."' in multioption statement")
300                        end
301                    end
302                    name = name:sub(-1)
303                end
304            end
305            if self._s2l[name] then
306                name = self._s2l[name]
307            end
308            if not self._opt[name] then
309                error("unknown option "..name)
310            end
311            opt = self._opt[name]
312            if opt.type == "string" then
313                need_arg = true
314            elseif opt.type == "number" then
315                if opt.counter then
316                    if opt.value == nil then
317                        opt.value = opt.default
318                    end
319                    opt.value = opt.value + 1
320                else
321                    need_arg = true
322                end
323            elseif opt.type == "boolean" then
324                if opt.value == nil then
325                    opt.value = opt.default
326                end
327                if opt.value then
328                    opt.value = false
329                else
330                    opt.value = true
331                end
332            else
333                error("internal error, invalid option type "..opt.type)
334            end
335        else
336            table.insert(left, args[n])
337        end
338    end
339
340    if need_arg then
341        error("option "..name.." needs argument")
342    end
343
344    for k, v in pairs(self._opt) do
345        if v.optional == false and v.value == nil then
346            error("missing required option "..k.."")
347        end
348    end
349
350    self.left = left
351    return left
352end
353
354-- Return the value of an option.
355function Getopt:val(name)
356    local opt = self._opt[name] or self._opt[self._s2l[name]]
357    if not opt then
358        return
359    end
360    if opt.value == nil then
361        return opt.default
362    else
363        return opt.value
364    end
365end
366
367-- dnsjit.core (3)
368return Getopt
369