123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527 |
- -- The MIT License (MIT)
-
- -- Copyright (c) 2013 - 2018 Peter Melnichenko
-
- -- Permission is hereby granted, free of charge, to any person obtaining a copy of
- -- this software and associated documentation files (the "Software"), to deal in
- -- the Software without restriction, including without limitation the rights to
- -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- -- the Software, and to permit persons to whom the Software is furnished to do so,
- -- subject to the following conditions:
-
- -- The above copyright notice and this permission notice shall be included in all
- -- copies or substantial portions of the Software.
-
- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- -- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- -- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- -- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- -- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
- local function deep_update(t1, t2)
- for k, v in pairs(t2) do
- if type(v) == "table" then
- v = deep_update({}, v)
- end
-
- t1[k] = v
- end
-
- return t1
- end
-
- -- A property is a tuple {name, callback}.
- -- properties.args is number of properties that can be set as arguments
- -- when calling an object.
- local function class(prototype, properties, parent)
- -- Class is the metatable of its instances.
- local cl = {}
- cl.__index = cl
-
- if parent then
- cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
- else
- cl.__prototype = prototype
- end
-
- if properties then
- local names = {}
-
- -- Create setter methods and fill set of property names.
- for _, property in ipairs(properties) do
- local name, callback = property[1], property[2]
-
- cl[name] = function(self, value)
- if not callback(self, value) then
- self["_" .. name] = value
- end
-
- return self
- end
-
- names[name] = true
- end
-
- function cl.__call(self, ...)
- -- When calling an object, if the first argument is a table,
- -- interpret keys as property names, else delegate arguments
- -- to corresponding setters in order.
- if type((...)) == "table" then
- for name, value in pairs((...)) do
- if names[name] then
- self[name](self, value)
- end
- end
- else
- local nargs = select("#", ...)
-
- for i, property in ipairs(properties) do
- if i > nargs or i > properties.args then
- break
- end
-
- local arg = select(i, ...)
-
- if arg ~= nil then
- self[property[1]](self, arg)
- end
- end
- end
-
- return self
- end
- end
-
- -- If indexing class fails, fallback to its parent.
- local class_metatable = {}
- class_metatable.__index = parent
-
- function class_metatable.__call(self, ...)
- -- Calling a class returns its instance.
- -- Arguments are delegated to the instance.
- local object = deep_update({}, self.__prototype)
- setmetatable(object, self)
- return object(...)
- end
-
- return setmetatable(cl, class_metatable)
- end
-
- local function typecheck(name, types, value)
- for _, type_ in ipairs(types) do
- if type(value) == type_ then
- return true
- end
- end
-
- error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value)))
- end
-
- local function typechecked(name, ...)
- local types = {...}
- return {name, function(_, value) typecheck(name, types, value) end}
- end
-
- local multiname = {"name", function(self, value)
- typecheck("name", {"string"}, value)
-
- for alias in value:gmatch("%S+") do
- self._name = self._name or alias
- table.insert(self._aliases, alias)
- end
-
- -- Do not set _name as with other properties.
- return true
- end}
-
- local function parse_boundaries(str)
- if tonumber(str) then
- return tonumber(str), tonumber(str)
- end
-
- if str == "*" then
- return 0, math.huge
- end
-
- if str == "+" then
- return 1, math.huge
- end
-
- if str == "?" then
- return 0, 1
- end
-
- if str:match "^%d+%-%d+$" then
- local min, max = str:match "^(%d+)%-(%d+)$"
- return tonumber(min), tonumber(max)
- end
-
- if str:match "^%d+%+$" then
- local min = str:match "^(%d+)%+$"
- return tonumber(min), math.huge
- end
- end
-
- local function boundaries(name)
- return {name, function(self, value)
- typecheck(name, {"number", "string"}, value)
-
- local min, max = parse_boundaries(value)
-
- if not min then
- error(("bad property '%s'"):format(name))
- end
-
- self["_min" .. name], self["_max" .. name] = min, max
- end}
- end
-
- local actions = {}
-
- local option_action = {"action", function(_, value)
- typecheck("action", {"function", "string"}, value)
-
- if type(value) == "string" and not actions[value] then
- error(("unknown action '%s'"):format(value))
- end
- end}
-
- local option_init = {"init", function(self)
- self._has_init = true
- end}
-
- local option_default = {"default", function(self, value)
- if type(value) ~= "string" then
- self._init = value
- self._has_init = true
- return true
- end
- end}
-
- local add_help = {"add_help", function(self, value)
- typecheck("add_help", {"boolean", "string", "table"}, value)
-
- if self._has_help then
- table.remove(self._options)
- self._has_help = false
- end
-
- if value then
- local help = self:flag()
- :description "Show this help message and exit."
- :action(function()
- print(self:get_help())
- os.exit(0)
- end)
-
- if value ~= true then
- help = help(value)
- end
-
- if not help._name then
- help "-h" "--help"
- end
-
- self._has_help = true
- end
- end}
-
- local Parser = class({
- _arguments = {},
- _options = {},
- _commands = {},
- _mutexes = {},
- _groups = {},
- _require_command = true,
- _handle_options = true
- }, {
- args = 3,
- typechecked("name", "string"),
- typechecked("description", "string"),
- typechecked("epilog", "string"),
- typechecked("usage", "string"),
- typechecked("help", "string"),
- typechecked("require_command", "boolean"),
- typechecked("handle_options", "boolean"),
- typechecked("action", "function"),
- typechecked("command_target", "string"),
- typechecked("help_vertical_space", "number"),
- typechecked("usage_margin", "number"),
- typechecked("usage_max_width", "number"),
- typechecked("help_usage_margin", "number"),
- typechecked("help_description_margin", "number"),
- typechecked("help_max_width", "number"),
- add_help
- })
-
- local Command = class({
- _aliases = {}
- }, {
- args = 3,
- multiname,
- typechecked("description", "string"),
- typechecked("epilog", "string"),
- typechecked("target", "string"),
- typechecked("usage", "string"),
- typechecked("help", "string"),
- typechecked("require_command", "boolean"),
- typechecked("handle_options", "boolean"),
- typechecked("action", "function"),
- typechecked("command_target", "string"),
- typechecked("help_vertical_space", "number"),
- typechecked("usage_margin", "number"),
- typechecked("usage_max_width", "number"),
- typechecked("help_usage_margin", "number"),
- typechecked("help_description_margin", "number"),
- typechecked("help_max_width", "number"),
- typechecked("hidden", "boolean"),
- add_help
- }, Parser)
-
- local Argument = class({
- _minargs = 1,
- _maxargs = 1,
- _mincount = 1,
- _maxcount = 1,
- _defmode = "unused",
- _show_default = true
- }, {
- args = 5,
- typechecked("name", "string"),
- typechecked("description", "string"),
- option_default,
- typechecked("convert", "function", "table"),
- boundaries("args"),
- typechecked("target", "string"),
- typechecked("defmode", "string"),
- typechecked("show_default", "boolean"),
- typechecked("argname", "string", "table"),
- typechecked("hidden", "boolean"),
- option_action,
- option_init
- })
-
- local Option = class({
- _aliases = {},
- _mincount = 0,
- _overwrite = true
- }, {
- args = 6,
- multiname,
- typechecked("description", "string"),
- option_default,
- typechecked("convert", "function", "table"),
- boundaries("args"),
- boundaries("count"),
- typechecked("target", "string"),
- typechecked("defmode", "string"),
- typechecked("show_default", "boolean"),
- typechecked("overwrite", "boolean"),
- typechecked("argname", "string", "table"),
- typechecked("hidden", "boolean"),
- option_action,
- option_init
- }, Argument)
-
- function Parser:_inherit_property(name, default)
- local element = self
-
- while true do
- local value = element["_" .. name]
-
- if value ~= nil then
- return value
- end
-
- if not element._parent then
- return default
- end
-
- element = element._parent
- end
- end
-
- function Argument:_get_argument_list()
- local buf = {}
- local i = 1
-
- while i <= math.min(self._minargs, 3) do
- local argname = self:_get_argname(i)
-
- if self._default and self._defmode:find "a" then
- argname = "[" .. argname .. "]"
- end
-
- table.insert(buf, argname)
- i = i+1
- end
-
- while i <= math.min(self._maxargs, 3) do
- table.insert(buf, "[" .. self:_get_argname(i) .. "]")
- i = i+1
-
- if self._maxargs == math.huge then
- break
- end
- end
-
- if i < self._maxargs then
- table.insert(buf, "...")
- end
-
- return buf
- end
-
- function Argument:_get_usage()
- local usage = table.concat(self:_get_argument_list(), " ")
-
- if self._default and self._defmode:find "u" then
- if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then
- usage = "[" .. usage .. "]"
- end
- end
-
- return usage
- end
-
- function actions.store_true(result, target)
- result[target] = true
- end
-
- function actions.store_false(result, target)
- result[target] = false
- end
-
- function actions.store(result, target, argument)
- result[target] = argument
- end
-
- function actions.count(result, target, _, overwrite)
- if not overwrite then
- result[target] = result[target] + 1
- end
- end
-
- function actions.append(result, target, argument, overwrite)
- result[target] = result[target] or {}
- table.insert(result[target], argument)
-
- if overwrite then
- table.remove(result[target], 1)
- end
- end
-
- function actions.concat(result, target, arguments, overwrite)
- if overwrite then
- error("'concat' action can't handle too many invocations")
- end
-
- result[target] = result[target] or {}
-
- for _, argument in ipairs(arguments) do
- table.insert(result[target], argument)
- end
- end
-
- function Argument:_get_action()
- local action, init
-
- if self._maxcount == 1 then
- if self._maxargs == 0 then
- action, init = "store_true", nil
- else
- action, init = "store", nil
- end
- else
- if self._maxargs == 0 then
- action, init = "count", 0
- else
- action, init = "append", {}
- end
- end
-
- if self._action then
- action = self._action
- end
-
- if self._has_init then
- init = self._init
- end
-
- if type(action) == "string" then
- action = actions[action]
- end
-
- return action, init
- end
-
- -- Returns placeholder for `narg`-th argument.
- function Argument:_get_argname(narg)
- local argname = self._argname or self:_get_default_argname()
-
- if type(argname) == "table" then
- return argname[narg]
- else
- return argname
- end
- end
-
- function Argument:_get_default_argname()
- return "<" .. self._name .. ">"
- end
-
- function Option:_get_default_argname()
- return "<" .. self:_get_default_target() .. ">"
- end
-
- -- Returns labels to be shown in the help message.
- function Argument:_get_label_lines()
- return {self._name}
- end
-
- function Option:_get_label_lines()
- local argument_list = self:_get_argument_list()
-
- if #argument_list == 0 then
- -- Don't put aliases for simple flags like `-h` on different lines.
- return {table.concat(self._aliases, ", ")}
- end
-
- local longest_alias_length = -1
-
- for _, alias in ipairs(self._aliases) do
- longest_alias_length = math.max(longest_alias_length, #alias)
- end
-
- local argument_list_repr = table.concat(argument_list, " ")
- local lines = {}
-
- for i, alias in ipairs(self._aliases) do
- local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr
-
- if i ~= #self._aliases then
- line = line .. ","
- end
-
- table.insert(lines, line)
- end
-
- return lines
- end
-
- function Command:_get_label_lines()
- return {table.concat(self._aliases, ", ")}
- end
-
- function Argument:_get_description()
- if self._default and self._show_default then
- if self._description then
- return ("%s (default: %s)"):format(self._description, self._default)
- else
- return ("default: %s"):format(self._default)
- end
- else
- return self._description or ""
- end
- end
-
- function Command:_get_description()
- return self._description or ""
- end
-
- function Option:_get_usage()
- local usage = self:_get_argument_list()
- table.insert(usage, 1, self._name)
- usage = table.concat(usage, " ")
-
- if self._mincount == 0 or self._default then
- usage = "[" .. usage .. "]"
- end
-
- return usage
- end
-
- function Argument:_get_default_target()
- return self._name
- end
-
- function Option:_get_default_target()
- local res
-
- for _, alias in ipairs(self._aliases) do
- if alias:sub(1, 1) == alias:sub(2, 2) then
- res = alias:sub(3)
- break
- end
- end
-
- res = res or self._name:sub(2)
- return (res:gsub("-", "_"))
- end
-
- function Option:_is_vararg()
- return self._maxargs ~= self._minargs
- end
-
- function Parser:_get_fullname()
- local parent = self._parent
- local buf = {self._name}
-
- while parent do
- table.insert(buf, 1, parent._name)
- parent = parent._parent
- end
-
- return table.concat(buf, " ")
- end
-
- function Parser:_update_charset(charset)
- charset = charset or {}
-
- for _, command in ipairs(self._commands) do
- command:_update_charset(charset)
- end
-
- for _, option in ipairs(self._options) do
- for _, alias in ipairs(option._aliases) do
- charset[alias:sub(1, 1)] = true
- end
- end
-
- return charset
- end
-
- function Parser:argument(...)
- local argument = Argument(...)
- table.insert(self._arguments, argument)
- return argument
- end
-
- function Parser:option(...)
- local option = Option(...)
-
- if self._has_help then
- table.insert(self._options, #self._options, option)
- else
- table.insert(self._options, option)
- end
-
- return option
- end
-
- function Parser:flag(...)
- return self:option():args(0)(...)
- end
-
- function Parser:command(...)
- local command = Command():add_help(true)(...)
- command._parent = self
- table.insert(self._commands, command)
- return command
- end
-
- function Parser:mutex(...)
- local elements = {...}
-
- for i, element in ipairs(elements) do
- local mt = getmetatable(element)
- assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i))
- end
-
- table.insert(self._mutexes, elements)
- return self
- end
-
- function Parser:group(name, ...)
- assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name)))
-
- local group = {name = name, ...}
-
- for i, element in ipairs(group) do
- local mt = getmetatable(element)
- assert(mt == Option or mt == Argument or mt == Command,
- ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1))
- end
-
- table.insert(self._groups, group)
- return self
- end
-
- local usage_welcome = "Usage: "
-
- function Parser:get_usage()
- if self._usage then
- return self._usage
- end
-
- local usage_margin = self:_inherit_property("usage_margin", #usage_welcome)
- local max_usage_width = self:_inherit_property("usage_max_width", 70)
- local lines = {usage_welcome .. self:_get_fullname()}
-
- local function add(s)
- if #lines[#lines]+1+#s <= max_usage_width then
- lines[#lines] = lines[#lines] .. " " .. s
- else
- lines[#lines+1] = (" "):rep(usage_margin) .. s
- end
- end
-
- -- Normally options are before positional arguments in usage messages.
- -- However, vararg options should be after, because they can't be reliable used
- -- before a positional argument.
- -- Mutexes come into play, too, and are shown as soon as possible.
- -- Overall, output usages in the following order:
- -- 1. Mutexes that don't have positional arguments or vararg options.
- -- 2. Options that are not in any mutexes and are not vararg.
- -- 3. Positional arguments - on their own or as a part of a mutex.
- -- 4. Remaining mutexes.
- -- 5. Remaining options.
-
- local elements_in_mutexes = {}
- local added_elements = {}
- local added_mutexes = {}
- local argument_to_mutexes = {}
-
- local function add_mutex(mutex, main_argument)
- if added_mutexes[mutex] then
- return
- end
-
- added_mutexes[mutex] = true
- local buf = {}
-
- for _, element in ipairs(mutex) do
- if not element._hidden and not added_elements[element] then
- if getmetatable(element) == Option or element == main_argument then
- table.insert(buf, element:_get_usage())
- added_elements[element] = true
- end
- end
- end
-
- if #buf == 1 then
- add(buf[1])
- elseif #buf > 1 then
- add("(" .. table.concat(buf, " | ") .. ")")
- end
- end
-
- local function add_element(element)
- if not element._hidden and not added_elements[element] then
- add(element:_get_usage())
- added_elements[element] = true
- end
- end
-
- for _, mutex in ipairs(self._mutexes) do
- local is_vararg = false
- local has_argument = false
-
- for _, element in ipairs(mutex) do
- if getmetatable(element) == Option then
- if element:_is_vararg() then
- is_vararg = true
- end
- else
- has_argument = true
- argument_to_mutexes[element] = argument_to_mutexes[element] or {}
- table.insert(argument_to_mutexes[element], mutex)
- end
-
- elements_in_mutexes[element] = true
- end
-
- if not is_vararg and not has_argument then
- add_mutex(mutex)
- end
- end
-
- for _, option in ipairs(self._options) do
- if not elements_in_mutexes[option] and not option:_is_vararg() then
- add_element(option)
- end
- end
-
- -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex.
- for _, argument in ipairs(self._arguments) do
- -- Pick a mutex as a part of which to show this argument, take the first one that's still available.
- local mutex
-
- if elements_in_mutexes[argument] then
- for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do
- if not added_mutexes[argument_mutex] then
- mutex = argument_mutex
- end
- end
- end
-
- if mutex then
- add_mutex(mutex, argument)
- else
- add_element(argument)
- end
- end
-
- for _, mutex in ipairs(self._mutexes) do
- add_mutex(mutex)
- end
-
- for _, option in ipairs(self._options) do
- add_element(option)
- end
-
- if #self._commands > 0 then
- if self._require_command then
- add("<command>")
- else
- add("[<command>]")
- end
-
- add("...")
- end
-
- return table.concat(lines, "\n")
- end
-
- local function split_lines(s)
- if s == "" then
- return {}
- end
-
- local lines = {}
-
- if s:sub(-1) ~= "\n" then
- s = s .. "\n"
- end
-
- for line in s:gmatch("([^\n]*)\n") do
- table.insert(lines, line)
- end
-
- return lines
- end
-
- local function autowrap_line(line, max_length)
- -- Algorithm for splitting lines is simple and greedy.
- local result_lines = {}
-
- -- Preserve original indentation of the line, put this at the beginning of each result line.
- -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts
- -- of the second and the following lines vertically align with the start of the second word.
- local indentation = line:match("^ *")
-
- if line:find("^ *[%*%+%-]") then
- indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)")
- end
-
- -- Parts of the last line being assembled.
- local line_parts = {}
-
- -- Length of the current line.
- local line_length = 0
-
- -- Index of the next character to consider.
- local index = 1
-
- while true do
- local word_start, word_finish, word = line:find("([^ ]+)", index)
-
- if not word_start then
- -- Ignore trailing spaces, if any.
- break
- end
-
- local preceding_spaces = line:sub(index, word_start - 1)
- index = word_finish + 1
-
- if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then
- -- Either this is the very first word or it fits as an addition to the current line, add it.
- table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation.
- table.insert(line_parts, word)
- line_length = line_length + #preceding_spaces + #word
- else
- -- Does not fit, finish current line and put the word into a new one.
- table.insert(result_lines, table.concat(line_parts))
- line_parts = {indentation, word}
- line_length = #indentation + #word
- end
- end
-
- if #line_parts > 0 then
- table.insert(result_lines, table.concat(line_parts))
- end
-
- if #result_lines == 0 then
- -- Preserve empty lines.
- result_lines[1] = ""
- end
-
- return result_lines
- end
-
- -- Automatically wraps lines within given array,
- -- attempting to limit line length to `max_length`.
- -- Existing line splits are preserved.
- local function autowrap(lines, max_length)
- local result_lines = {}
-
- for _, line in ipairs(lines) do
- local autowrapped_lines = autowrap_line(line, max_length)
-
- for _, autowrapped_line in ipairs(autowrapped_lines) do
- table.insert(result_lines, autowrapped_line)
- end
- end
-
- return result_lines
- end
-
- function Parser:_get_element_help(element)
- local label_lines = element:_get_label_lines()
- local description_lines = split_lines(element:_get_description())
-
- local result_lines = {}
-
- -- All label lines should have the same length (except the last one, it has no comma).
- -- If too long, start description after all the label lines.
- -- Otherwise, combine label and description lines.
-
- local usage_margin_len = self:_inherit_property("help_usage_margin", 3)
- local usage_margin = (" "):rep(usage_margin_len)
- local description_margin_len = self:_inherit_property("help_description_margin", 25)
- local description_margin = (" "):rep(description_margin_len)
-
- local help_max_width = self:_inherit_property("help_max_width")
-
- if help_max_width then
- local description_max_width = math.max(help_max_width - description_margin_len, 10)
- description_lines = autowrap(description_lines, description_max_width)
- end
-
- if #label_lines[1] >= (description_margin_len - usage_margin_len) then
- for _, label_line in ipairs(label_lines) do
- table.insert(result_lines, usage_margin .. label_line)
- end
-
- for _, description_line in ipairs(description_lines) do
- table.insert(result_lines, description_margin .. description_line)
- end
- else
- for i = 1, math.max(#label_lines, #description_lines) do
- local label_line = label_lines[i]
- local description_line = description_lines[i]
-
- local line = ""
-
- if label_line then
- line = usage_margin .. label_line
- end
-
- if description_line and description_line ~= "" then
- line = line .. (" "):rep(description_margin_len - #line) .. description_line
- end
-
- table.insert(result_lines, line)
- end
- end
-
- return table.concat(result_lines, "\n")
- end
-
- local function get_group_types(group)
- local types = {}
-
- for _, element in ipairs(group) do
- types[getmetatable(element)] = true
- end
-
- return types
- end
-
- function Parser:_add_group_help(blocks, added_elements, label, elements)
- local buf = {label}
-
- for _, element in ipairs(elements) do
- if not element._hidden and not added_elements[element] then
- added_elements[element] = true
- table.insert(buf, self:_get_element_help(element))
- end
- end
-
- if #buf > 1 then
- table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1)))
- end
- end
-
- function Parser:get_help()
- if self._help then
- return self._help
- end
-
- local blocks = {self:get_usage()}
-
- local help_max_width = self:_inherit_property("help_max_width")
-
- if self._description then
- local description = self._description
-
- if help_max_width then
- description = table.concat(autowrap(split_lines(description), help_max_width), "\n")
- end
-
- table.insert(blocks, description)
- end
-
- -- 1. Put groups containing arguments first, then other arguments.
- -- 2. Put remaining groups containing options, then other options.
- -- 3. Put remaining groups containing commands, then other commands.
- -- Assume that an element can't be in several groups.
- local groups_by_type = {
- [Argument] = {},
- [Option] = {},
- [Command] = {}
- }
-
- for _, group in ipairs(self._groups) do
- local group_types = get_group_types(group)
-
- for _, mt in ipairs({Argument, Option, Command}) do
- if group_types[mt] then
- table.insert(groups_by_type[mt], group)
- break
- end
- end
- end
-
- local default_groups = {
- {name = "Arguments", type = Argument, elements = self._arguments},
- {name = "Options", type = Option, elements = self._options},
- {name = "Commands", type = Command, elements = self._commands}
- }
-
- local added_elements = {}
-
- for _, default_group in ipairs(default_groups) do
- local type_groups = groups_by_type[default_group.type]
-
- for _, group in ipairs(type_groups) do
- self:_add_group_help(blocks, added_elements, group.name .. ":", group)
- end
-
- local default_label = default_group.name .. ":"
-
- if #type_groups > 0 then
- default_label = "Other " .. default_label:gsub("^.", string.lower)
- end
-
- self:_add_group_help(blocks, added_elements, default_label, default_group.elements)
- end
-
- if self._epilog then
- local epilog = self._epilog
-
- if help_max_width then
- epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n")
- end
-
- table.insert(blocks, epilog)
- end
-
- return table.concat(blocks, "\n\n")
- end
-
- local function get_tip(context, wrong_name)
- local context_pool = {}
- local possible_name
- local possible_names = {}
-
- for name in pairs(context) do
- if type(name) == "string" then
- for i = 1, #name do
- possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
-
- if not context_pool[possible_name] then
- context_pool[possible_name] = {}
- end
-
- table.insert(context_pool[possible_name], name)
- end
- end
- end
-
- for i = 1, #wrong_name + 1 do
- possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1)
-
- if context[possible_name] then
- possible_names[possible_name] = true
- elseif context_pool[possible_name] then
- for _, name in ipairs(context_pool[possible_name]) do
- possible_names[name] = true
- end
- end
- end
-
- local first = next(possible_names)
-
- if first then
- if next(possible_names, first) then
- local possible_names_arr = {}
-
- for name in pairs(possible_names) do
- table.insert(possible_names_arr, "'" .. name .. "'")
- end
-
- table.sort(possible_names_arr)
- return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?"
- else
- return "\nDid you mean '" .. first .. "'?"
- end
- else
- return ""
- end
- end
-
- local ElementState = class({
- invocations = 0
- })
-
- function ElementState:__call(state, element)
- self.state = state
- self.result = state.result
- self.element = element
- self.target = element._target or element:_get_default_target()
- self.action, self.result[self.target] = element:_get_action()
- return self
- end
-
- function ElementState:error(fmt, ...)
- self.state:error(fmt, ...)
- end
-
- function ElementState:convert(argument, index)
- local converter = self.element._convert
-
- if converter then
- local ok, err
-
- if type(converter) == "function" then
- ok, err = converter(argument)
- elseif type(converter[index]) == "function" then
- ok, err = converter[index](argument)
- else
- ok = converter[argument]
- end
-
- if ok == nil then
- self:error(err and "%s" or "malformed argument '%s'", err or argument)
- end
-
- argument = ok
- end
-
- return argument
- end
-
- function ElementState:default(mode)
- return self.element._defmode:find(mode) and self.element._default
- end
-
- local function bound(noun, min, max, is_max)
- local res = ""
-
- if min ~= max then
- res = "at " .. (is_max and "most" or "least") .. " "
- end
-
- local number = is_max and max or min
- return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s")
- end
-
- function ElementState:set_name(alias)
- self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name)
- end
-
- function ElementState:invoke()
- self.open = true
- self.overwrite = false
-
- if self.invocations >= self.element._maxcount then
- if self.element._overwrite then
- self.overwrite = true
- else
- local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true)
- self:error("%s must be used %s", self.name, num_times_repr)
- end
- else
- self.invocations = self.invocations + 1
- end
-
- self.args = {}
-
- if self.element._maxargs <= 0 then
- self:close()
- end
-
- return self.open
- end
-
- function ElementState:pass(argument)
- argument = self:convert(argument, #self.args + 1)
- table.insert(self.args, argument)
-
- if #self.args >= self.element._maxargs then
- self:close()
- end
-
- return self.open
- end
-
- function ElementState:complete_invocation()
- while #self.args < self.element._minargs do
- self:pass(self.element._default)
- end
- end
-
- function ElementState:close()
- if self.open then
- self.open = false
-
- if #self.args < self.element._minargs then
- if self:default("a") then
- self:complete_invocation()
- else
- if #self.args == 0 then
- if getmetatable(self.element) == Argument then
- self:error("missing %s", self.name)
- elseif self.element._maxargs == 1 then
- self:error("%s requires an argument", self.name)
- end
- end
-
- self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs))
- end
- end
-
- local args
-
- if self.element._maxargs == 0 then
- args = self.args[1]
- elseif self.element._maxargs == 1 then
- if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then
- args = self.args
- else
- args = self.args[1]
- end
- else
- args = self.args
- end
-
- self.action(self.result, self.target, args, self.overwrite)
- end
- end
-
- local ParseState = class({
- result = {},
- options = {},
- arguments = {},
- argument_i = 1,
- element_to_mutexes = {},
- mutex_to_element_state = {},
- command_actions = {}
- })
-
- function ParseState:__call(parser, error_handler)
- self.parser = parser
- self.error_handler = error_handler
- self.charset = parser:_update_charset()
- self:switch(parser)
- return self
- end
-
- function ParseState:error(fmt, ...)
- self.error_handler(self.parser, fmt:format(...))
- end
-
- function ParseState:switch(parser)
- self.parser = parser
-
- if parser._action then
- table.insert(self.command_actions, {action = parser._action, name = parser._name})
- end
-
- for _, option in ipairs(parser._options) do
- option = ElementState(self, option)
- table.insert(self.options, option)
-
- for _, alias in ipairs(option.element._aliases) do
- self.options[alias] = option
- end
- end
-
- for _, mutex in ipairs(parser._mutexes) do
- for _, element in ipairs(mutex) do
- if not self.element_to_mutexes[element] then
- self.element_to_mutexes[element] = {}
- end
-
- table.insert(self.element_to_mutexes[element], mutex)
- end
- end
-
- for _, argument in ipairs(parser._arguments) do
- argument = ElementState(self, argument)
- table.insert(self.arguments, argument)
- argument:set_name()
- argument:invoke()
- end
-
- self.handle_options = parser._handle_options
- self.argument = self.arguments[self.argument_i]
- self.commands = parser._commands
-
- for _, command in ipairs(self.commands) do
- for _, alias in ipairs(command._aliases) do
- self.commands[alias] = command
- end
- end
- end
-
- function ParseState:get_option(name)
- local option = self.options[name]
-
- if not option then
- self:error("unknown option '%s'%s", name, get_tip(self.options, name))
- else
- return option
- end
- end
-
- function ParseState:get_command(name)
- local command = self.commands[name]
-
- if not command then
- if #self.commands > 0 then
- self:error("unknown command '%s'%s", name, get_tip(self.commands, name))
- else
- self:error("too many arguments")
- end
- else
- return command
- end
- end
-
- function ParseState:check_mutexes(element_state)
- if self.element_to_mutexes[element_state.element] then
- for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do
- local used_element_state = self.mutex_to_element_state[mutex]
-
- if used_element_state and used_element_state ~= element_state then
- self:error("%s can not be used together with %s", element_state.name, used_element_state.name)
- else
- self.mutex_to_element_state[mutex] = element_state
- end
- end
- end
- end
-
- function ParseState:invoke(option, name)
- self:close()
- option:set_name(name)
- self:check_mutexes(option, name)
-
- if option:invoke() then
- self.option = option
- end
- end
-
- function ParseState:pass(arg)
- if self.option then
- if not self.option:pass(arg) then
- self.option = nil
- end
- elseif self.argument then
- self:check_mutexes(self.argument)
-
- if not self.argument:pass(arg) then
- self.argument_i = self.argument_i + 1
- self.argument = self.arguments[self.argument_i]
- end
- else
- local command = self:get_command(arg)
- self.result[command._target or command._name] = true
-
- if self.parser._command_target then
- self.result[self.parser._command_target] = command._name
- end
-
- self:switch(command)
- end
- end
-
- function ParseState:close()
- if self.option then
- self.option:close()
- self.option = nil
- end
- end
-
- function ParseState:finalize()
- self:close()
-
- for i = self.argument_i, #self.arguments do
- local argument = self.arguments[i]
- if #argument.args == 0 and argument:default("u") then
- argument:complete_invocation()
- else
- argument:close()
- end
- end
-
- if self.parser._require_command and #self.commands > 0 then
- self:error("a command is required")
- end
-
- for _, option in ipairs(self.options) do
- option.name = option.name or ("option '%s'"):format(option.element._name)
-
- if option.invocations == 0 then
- if option:default("u") then
- option:invoke()
- option:complete_invocation()
- option:close()
- end
- end
-
- local mincount = option.element._mincount
-
- if option.invocations < mincount then
- if option:default("a") then
- while option.invocations < mincount do
- option:invoke()
- option:close()
- end
- elseif option.invocations == 0 then
- self:error("missing %s", option.name)
- else
- self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount))
- end
- end
- end
-
- for i = #self.command_actions, 1, -1 do
- self.command_actions[i].action(self.result, self.command_actions[i].name)
- end
- end
-
- function ParseState:parse(args)
- for _, arg in ipairs(args) do
- local plain = true
-
- if self.handle_options then
- local first = arg:sub(1, 1)
-
- if self.charset[first] then
- if #arg > 1 then
- plain = false
-
- if arg:sub(2, 2) == first then
- if #arg == 2 then
- if self.options[arg] then
- local option = self:get_option(arg)
- self:invoke(option, arg)
- else
- self:close()
- end
-
- self.handle_options = false
- else
- local equals = arg:find "="
- if equals then
- local name = arg:sub(1, equals - 1)
- local option = self:get_option(name)
-
- if option.element._maxargs <= 0 then
- self:error("option '%s' does not take arguments", name)
- end
-
- self:invoke(option, name)
- self:pass(arg:sub(equals + 1))
- else
- local option = self:get_option(arg)
- self:invoke(option, arg)
- end
- end
- else
- for i = 2, #arg do
- local name = first .. arg:sub(i, i)
- local option = self:get_option(name)
- self:invoke(option, name)
-
- if i ~= #arg and option.element._maxargs > 0 then
- self:pass(arg:sub(i + 1))
- break
- end
- end
- end
- end
- end
- end
-
- if plain then
- self:pass(arg)
- end
- end
-
- self:finalize()
- return self.result
- end
-
- function Parser:error(msg)
- io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg))
- os.exit(1)
- end
-
- -- Compatibility with strict.lua and other checkers:
- local default_cmdline = rawget(_G, "arg") or {}
-
- function Parser:_parse(args, error_handler)
- return ParseState(self, error_handler):parse(args or default_cmdline)
- end
-
- function Parser:parse(args)
- return self:_parse(args, self.error)
- end
-
- local function xpcall_error_handler(err)
- return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2)
- end
-
- function Parser:pparse(args)
- local parse_error
-
- local ok, result = xpcall(function()
- return self:_parse(args, function(_, err)
- parse_error = err
- error(err, 0)
- end)
- end, xpcall_error_handler)
-
- if ok then
- return true, result
- elseif not parse_error then
- error(result, 0)
- else
- return false, parse_error
- end
- end
-
- local argparse = {}
-
- argparse.version = "0.6.0"
-
- setmetatable(argparse, {__call = function(_, ...)
- return Parser(default_cmdline[0]):add_help(true)(...)
- end})
-
- return argparse
|