dnsjit/src/lib/getopt.lua

369 lines
10 KiB
Lua
Raw Normal View History

-- Copyright (c) 2018-2021, 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 <http://www.gnu.org/licenses/>.
-- 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