]> source.dussan.org Git - rspamd.git/commitdiff
[Minor] Import lua-argparse library
authorVsevolod Stakhov <vsevolod@highsecure.ru>
Wed, 23 May 2018 16:54:17 +0000 (17:54 +0100)
committerVsevolod Stakhov <vsevolod@highsecure.ru>
Wed, 23 May 2018 16:54:17 +0000 (17:54 +0100)
CMakeLists.txt
contrib/lua-argparse/LICENSE [new file with mode: 0644]
contrib/lua-argparse/argparse.lua [new file with mode: 0644]

index b88da4241f29029c1739e7d1d983162aead3b801..7a9373faaf6827cccc9fbd7dad0a8bcd8c173df2 100644 (file)
@@ -1358,6 +1358,7 @@ ENDFOREACH(LUA_LIB)
 
 # Install lua fun library
 INSTALL(FILES "contrib/lua-fun/fun.lua" DESTINATION ${LUALIBDIR})
+INSTALL(FILES "contrib/lua-argparse/argparse.lua" DESTINATION ${LUALIBDIR})
 
 IF(ENABLE_TORCH MATCHES "ON")
        INSTALL(FILES "contrib/moses/moses.lua" DESTINATION ${LUALIBDIR})
diff --git a/contrib/lua-argparse/LICENSE b/contrib/lua-argparse/LICENSE
new file mode 100644 (file)
index 0000000..87579ac
--- /dev/null
@@ -0,0 +1,20 @@
+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.
diff --git a/contrib/lua-argparse/argparse.lua b/contrib/lua-argparse/argparse.lua
new file mode 100644 (file)
index 0000000..402f14a
--- /dev/null
@@ -0,0 +1,1527 @@
+-- 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