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