-- Copyright (c) 2018-2023, OARC, Inc. -- All rights reserved. -- -- This file is part of dnsjit. -- -- dnsjit is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- dnsjit is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with dnsjit. If not, see . -- dnsjit.lib.getopt -- Parse and handle arguments -- local getopt = require("dnsjit.lib.getopt").new({ -- { "v", "verbose", 0, "Enable verbosity", "?+" }, -- { nil, "host", "localhost", "Set host", "?" }, -- { "p", nil, 53, "Set port", "?" }, -- }) -- . -- local left = getopt:parse() -- . -- print("host", getopt:val("host")) -- print("port", getopt:val("p")) -- -- A "getopt long" implementation to easily handle command line arguments -- and display usage. -- An option is the short name (one character), long name, -- default value (which also defines the type), help text and extensions. -- Options are by default required, see extensions to change this. -- .LP -- The Lua types allowed are -- .BR boolean , -- .BR string , -- .BR number . -- .LP -- The extensions available are: -- .TP -- .B ? -- Make the option optional. -- .TP -- .B * -- For string and number options this make it possible to specified it -- multiple times and all values will be returned in a table. -- .TP -- .B + -- For number options this will act as an counter increaser, the value will -- be the default value + 1 for each time the option is given. -- .LP -- Option -- .I -h -- and -- .I --help -- are automatically added if the option -- .I --help -- is not already defined. -- .SS Attributes -- .TP -- left -- A table that contains the arguments left after parsing, same as returned by -- .IR parse() . -- .TP -- usage_desc -- A string that describes the usage of the program, if not set then the -- default will be " -- .I "program [options...]" -- ". module(...,package.seeall) local log = require("dnsjit.core.log") local module_log = log.new("lib.getopt") Getopt = {} -- Create a new Getopt object. -- .I args -- is a table with tables that specifies the options available. -- Each entry is unpacked and sent to -- .BR Getopt:add() . function Getopt.new(args) local self = setmetatable({ left = {}, usage_desc = nil, _opt = {}, _s2l = {}, _log = log.new("lib.getopt", module_log), }, { __index = Getopt }) self._log:debug("new()") for k, v in pairs(args) do local short, long, default, help, extensions = unpack(v) self:add(short, long, default, help, extensions) end return self end -- Return the Log object to control logging of this instance or module. function Getopt:log() if self == nil then return module_log end return self._log end -- Add an option. function Getopt:add(short, long, default, help, extensions) local optional = false local multiple = false local counter = false local name = long or short if type(name) ~= "string" then error("long|short) need to be a string") elseif name == "" then error("name (long|short) needs to be set") end if short and (type(short) ~= "string" or #short ~= 1) then error("short needs to be a string of length 1") end if self._opt[name] then error("option "..name.." alredy exists") elseif short and self._s2l[short] then error("option "..short.." alredy exists") end local t = type(default) if t ~= "string" and t ~= "number" and t ~= "boolean" then error("option "..name..": invalid type "..t) end if type(extensions) == "string" then local n for n = 1, extensions:len() do local extension = extensions:sub(n, n) if extension == "?" then optional = true elseif extension == "*" then multiple = true elseif extension == "+" then counter = true else error("option "..name..": invalid extension "..extension) end end end self._opt[name] = { value = nil, short = short, long = long, type = t, default = default, help = help, optional = optional, multiple = multiple, counter = counter, } if long and short then self._s2l[short] = long elseif short and not long then self._s2l[short] = short end if not self._opt["help"] then self._opt["help"] = { short = nil, long = "help", type = "boolean", default = false, help = "Display this help text", optional = true, } if not self._s2l["h"] then self._opt["help"].short = "h" self._s2l["h"] = "help" end end end -- Print the usage. function Getopt:usage() if self.usage_desc then print("usage: " .. self.usage_desc) else print("usage: program [options...]") end local opts = {} for k, _ in pairs(self._opt) do if k ~= "help" then table.insert(opts, k) end end table.sort(opts) table.insert(opts, "help") for _, k in pairs(opts) do local v = self._opt[k] local arg if v.type == "string" then arg = " \""..v.default.."\"" elseif v.type == "number" and v.counter == false then arg = " "..v.default else arg = "" end if v.long then print("", (v.short and "-"..v.short or " ").." --"..v.long..arg, v.help) else print("", "-"..v.short..arg, v.help) end end end -- Parse the options. -- If -- .I args -- is not specified or nil then the global -- .B arg -- is used. -- If -- .I startn -- is given, it will start parsing arguments in the table from that position. -- The default position to start at is 2 for -- .IR dnsjit , -- see -- .BR dnsjit.core (3). function Getopt:parse(args, startn) if not args then args = arg end local n local opt = nil local left = {} local need_arg = false local stop = false local name for n = startn or 2, table.maxn(args) do if need_arg then if opt.multiple then if opt.value == nil then opt.value = {} end if opt.type == "number" then table.insert(opt.value, tonumber(args[n])) else table.insert(opt.value, args[n]) end else if opt.type == "number" then opt.value = tonumber(args[n]) else opt.value = args[n] end end need_arg = false elseif stop or args[n] == "-" then table.insert(left, args[n]) elseif args[n] == "--" then stop = true elseif args[n]:sub(1, 1) == "-" then if args[n]:sub(1, 2) == "--" then name = args[n]:sub(3) else name = args[n]:sub(2) if name:len() > 1 then local n2, name2 for n2 = 1, name:len() - 1 do name2 = name:sub(n2, n2) opt = self._opt[self._s2l[name2]] if not opt then error("unknown option "..name2) end if opt.type == "number" and opt.counter then if opt.value == nil then opt.value = opt.default end opt.value = opt.value + 1 elseif opt.type == "boolean" then if opt.value == nil then opt.value = opt.default end if opt.value then opt.value = false else opt.value = true end else error("invalid short option '"..name2.."' in multioption statement") end end name = name:sub(-1) end end if self._s2l[name] then name = self._s2l[name] end if not self._opt[name] then error("unknown option "..name) end opt = self._opt[name] if opt.type == "string" then need_arg = true elseif opt.type == "number" then if opt.counter then if opt.value == nil then opt.value = opt.default end opt.value = opt.value + 1 else need_arg = true end elseif opt.type == "boolean" then if opt.value == nil then opt.value = opt.default end if opt.value then opt.value = false else opt.value = true end else error("internal error, invalid option type "..opt.type) end else table.insert(left, args[n]) end end if need_arg then error("option "..name.." needs argument") end for k, v in pairs(self._opt) do if v.optional == false and v.value == nil then error("missing required option "..k.."") end end self.left = left return left end -- Return the value of an option. function Getopt:val(name) local opt = self._opt[name] or self._opt[self._s2l[name]] if not opt then return end if opt.value == nil then return opt.default else return opt.value end end -- dnsjit.core (3) return Getopt