1
0
mirror of https://github.com/Adorable-Catgirl/LuaComp.git synced 2024-11-01 08:00:56 +11:00
LuaComp/src/argparse.lua

1528 lines
39 KiB
Lua
Raw Normal View History

2019-11-06 06:47:42 +11:00
-- 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