-- The MIT License (MIT) -- Copyright (c) 2013 - 2018 Peter Melnichenko -- 2019 Paul Ouellette -- 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) table.insert(self._public_aliases, alias) -- If alias contains '_', accept '-' also. if alias:find("_", 1, true) then table.insert(self._aliases, (alias:gsub("_", "-"))) end end -- Do not set _name as with other properties. return true end} local multiname_hidden = {"hidden_name", function(self, value) typecheck("hidden_name", {"string"}, value) for alias in value:gmatch("%S+") do table.insert(self._aliases, alias) if alias:find("_", 1, true) then table.insert(self._aliases, (alias:gsub("_", "-"))) end end 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._help_option_idx then table.remove(self._options, self._help_option_idx) self._help_option_idx = nil 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._help_option_idx = #self._options 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 = {}, _public_aliases = {} }, { args = 3, multiname, typechecked("description", "string"), typechecked("epilog", "string"), multiname_hidden, typechecked("summary", "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("choices", "table"), typechecked("hidden", "boolean"), option_action, option_init }) local Option = class({ _aliases = {}, _public_aliases = {}, _mincount = 0, _overwrite = true }, { args = 6, multiname, typechecked("description", "string"), option_default, typechecked("convert", "function", "table"), boundaries("args"), boundaries("count"), multiname_hidden, typechecked("target", "string"), typechecked("defmode", "string"), typechecked("show_default", "boolean"), typechecked("overwrite", "boolean"), typechecked("argname", "string", "table"), typechecked("choices", "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_choices_list() return "{" .. table.concat(self._choices, ",") .. "}" end function Argument:_get_default_argname() if self._choices then return self:_get_choices_list() else return "<" .. self._name .. ">" end end function Option:_get_default_argname() if self._choices then return self:_get_choices_list() else return "<" .. self:_get_default_target() .. ">" end end -- Returns labels to be shown in the help message. function Argument:_get_label_lines() if self._choices then return {self:_get_choices_list()} else return {self._name} end 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._public_aliases, ", ")} end local longest_alias_length = -1 for _, alias in ipairs(self._public_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._public_aliases) do local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr if i ~= #self._public_aliases then line = line .. "," end table.insert(lines, line) end return lines end function Command:_get_label_lines() return {table.concat(self._public_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._summary or 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._public_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(exclude_root) local parent = self._parent if exclude_root and not parent then return "" end local buf = {self._name} while parent do if not exclude_root or parent._parent then table.insert(buf, 1, parent._name) end 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(...) table.insert(self._options, option) 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("") else add("[]") 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 function Parser:add_help_command(value) if value then assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_help_command' (string or table expected, got %s)"):format(type(value))) end local help = self:command() :description "Show help for commands." help:argument "command" :description "The command to show help for." :args "?" :action(function(_, _, cmd) if not cmd then print(self:get_help()) os.exit(0) else for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do if alias == cmd then print(command:get_help()) os.exit(0) end end end end help:error(("unknown command '%s'"):format(cmd)) end) if value then help = help(value) end if not help._name then help "help" end help._is_help_command = true return self end function Parser:_is_shell_safe() if self._basename then if self._basename:find("[^%w_%-%+%.]") then return false end else for _, alias in ipairs(self._aliases) do if alias:find("[^%w_%-%+%.]") then return false end end end for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do if alias:find("[^%w_%-%+%.]") then return false end end if option._choices then for _, choice in ipairs(option._choices) do if choice:find("[%s'\"]") then return false end end end end for _, argument in ipairs(self._arguments) do if argument._choices then for _, choice in ipairs(argument._choices) do if choice:find("[%s'\"]") then return false end end end end for _, command in ipairs(self._commands) do if not command:_is_shell_safe() then return false end end return true end function Parser:add_complete(value) if value then assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) end local complete = self:option() :description "Output a shell completion script for the specified shell." :args(1) :choices {"bash", "zsh", "fish"} :action(function(_, _, shell) io.write(self["get_" .. shell .. "_complete"](self)) os.exit(0) end) if value then complete = complete(value) end if not complete._name then complete "--completion" end return self end function Parser:add_complete_command(value) if value then assert(type(value) == "string" or type(value) == "table", ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) end local complete = self:command() :description "Output a shell completion script." complete:argument "shell" :description "The shell to output a completion script for." :choices {"bash", "zsh", "fish"} :action(function(_, _, shell) io.write(self["get_" .. shell .. "_complete"](self)) os.exit(0) end) if value then complete = complete(value) end if not complete._name then complete "completion" end return self end local function base_name(pathname) return pathname:gsub("[/\\]*$", ""):match(".*[/\\]([^/\\]*)") or pathname end local function get_short_description(element) local short = element:_get_description():match("^(.-)%.%s") return short or element:_get_description():match("^(.-)%.?$") end function Parser:_get_options() local options = {} for _, option in ipairs(self._options) do for _, alias in ipairs(option._aliases) do table.insert(options, alias) end end return table.concat(options, " ") end function Parser:_get_commands() local commands = {} for _, command in ipairs(self._commands) do for _, alias in ipairs(command._aliases) do table.insert(commands, alias) end end return table.concat(commands, " ") end function Parser:_bash_option_args(buf, indent) local opts = {} for _, option in ipairs(self._options) do if option._choices or option._minargs > 0 then local compreply if option._choices then compreply = 'COMPREPLY=($(compgen -W "' .. table.concat(option._choices, " ") .. '" -- "$cur"))' else compreply = 'COMPREPLY=($(compgen -f -- "$cur"))' end table.insert(opts, (" "):rep(indent + 4) .. table.concat(option._aliases, "|") .. ")") table.insert(opts, (" "):rep(indent + 8) .. compreply) table.insert(opts, (" "):rep(indent + 8) .. "return 0") table.insert(opts, (" "):rep(indent + 8) .. ";;") end end if #opts > 0 then table.insert(buf, (" "):rep(indent) .. 'case "$prev" in') table.insert(buf, table.concat(opts, "\n")) table.insert(buf, (" "):rep(indent) .. "esac\n") end end function Parser:_bash_get_cmd(buf, indent) if #self._commands == 0 then return end table.insert(buf, (" "):rep(indent) .. 'args=("${args[@]:1}")') table.insert(buf, (" "):rep(indent) .. 'for arg in "${args[@]}"; do') table.insert(buf, (" "):rep(indent + 4) .. 'case "$arg" in') for _, command in ipairs(self._commands) do table.insert(buf, (" "):rep(indent + 8) .. table.concat(command._aliases, "|") .. ")") if self._parent then table.insert(buf, (" "):rep(indent + 12) .. 'cmd="$cmd ' .. command._name .. '"') else table.insert(buf, (" "):rep(indent + 12) .. 'cmd="' .. command._name .. '"') end table.insert(buf, (" "):rep(indent + 12) .. 'opts="$opts ' .. command:_get_options() .. '"') command:_bash_get_cmd(buf, indent + 12) table.insert(buf, (" "):rep(indent + 12) .. "break") table.insert(buf, (" "):rep(indent + 12) .. ";;") end table.insert(buf, (" "):rep(indent + 4) .. "esac") table.insert(buf, (" "):rep(indent) .. "done") end function Parser:_bash_cmd_completions(buf) local cmd_buf = {} if self._parent then self:_bash_option_args(cmd_buf, 12) end if #self._commands > 0 then table.insert(cmd_buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self:_get_commands() .. '" -- "$cur"))') elseif self._is_help_command then table.insert(cmd_buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self._parent:_get_commands() .. '" -- "$cur"))') end if #cmd_buf > 0 then table.insert(buf, (" "):rep(8) .. "'" .. self:_get_fullname(true) .. "')") table.insert(buf, table.concat(cmd_buf, "\n")) table.insert(buf, (" "):rep(12) .. ";;") end for _, command in ipairs(self._commands) do command:_bash_cmd_completions(buf) end end function Parser:get_bash_complete() self._basename = base_name(self._name) assert(self:_is_shell_safe()) local buf = {([[ _%s() { local IFS=$' \t\n' local args cur prev cmd opts arg args=("${COMP_WORDS[@]}") cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts="%s" ]]):format(self._basename, self:_get_options())} self:_bash_option_args(buf, 4) self:_bash_get_cmd(buf, 4) if #self._commands > 0 then table.insert(buf, "") table.insert(buf, (" "):rep(4) .. 'case "$cmd" in') self:_bash_cmd_completions(buf) table.insert(buf, (" "):rep(4) .. "esac\n") end table.insert(buf, ([=[ if [[ "$cur" = -* ]]; then COMPREPLY=($(compgen -W "$opts" -- "$cur")) fi } complete -F _%s -o bashdefault -o default %s ]=]):format(self._basename, self._basename)) return table.concat(buf, "\n") end function Parser:_zsh_arguments(buf, cmd_name, indent) if self._parent then table.insert(buf, (" "):rep(indent) .. "options=(") table.insert(buf, (" "):rep(indent + 2) .. "$options") else table.insert(buf, (" "):rep(indent) .. "local -a options=(") end for _, option in ipairs(self._options) do local line = {} if #option._aliases > 1 then if option._maxcount > 1 then table.insert(line, '"*"') end table.insert(line, "{" .. table.concat(option._aliases, ",") .. '}"') else table.insert(line, '"') if option._maxcount > 1 then table.insert(line, "*") end table.insert(line, option._name) end if option._description then local description = get_short_description(option):gsub('["%]:`$]', "\\%0") table.insert(line, "[" .. description .. "]") end if option._maxargs == math.huge then table.insert(line, ":*") end if option._choices then table.insert(line, ": :(" .. table.concat(option._choices, " ") .. ")") elseif option._maxargs > 0 then table.insert(line, ": :_files") end table.insert(line, '"') table.insert(buf, (" "):rep(indent + 2) .. table.concat(line)) end table.insert(buf, (" "):rep(indent) .. ")") table.insert(buf, (" "):rep(indent) .. "_arguments -s -S \\") table.insert(buf, (" "):rep(indent + 2) .. "$options \\") if self._is_help_command then table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. self._parent:_get_commands() .. ')" \\') else for _, argument in ipairs(self._arguments) do local spec if argument._choices then spec = ": :(" .. table.concat(argument._choices, " ") .. ")" else spec = ": :_files" end if argument._maxargs == math.huge then table.insert(buf, (" "):rep(indent + 2) .. '"*' .. spec .. '" \\') break end for _ = 1, argument._maxargs do table.insert(buf, (" "):rep(indent + 2) .. '"' .. spec .. '" \\') end end if #self._commands > 0 then table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\') table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\') end end table.insert(buf, (" "):rep(indent + 2) .. "&& return 0") end function Parser:_zsh_cmds(buf, cmd_name) table.insert(buf, "\n_" .. cmd_name .. "_cmds() {") table.insert(buf, " local -a commands=(") for _, command in ipairs(self._commands) do local line = {} if #command._aliases > 1 then table.insert(line, "{" .. table.concat(command._aliases, ",") .. '}"') else table.insert(line, '"' .. command._name) end if command._description then table.insert(line, ":" .. get_short_description(command):gsub('["`$]', "\\%0")) end table.insert(buf, " " .. table.concat(line) .. '"') end table.insert(buf, ' )\n _describe "command" commands\n}') end function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) if #self._commands == 0 then return end self:_zsh_cmds(cmds_buf, cmd_name) table.insert(buf, "\n" .. (" "):rep(indent) .. "case $words[1] in") for _, command in ipairs(self._commands) do local name = cmd_name .. "_" .. command._name table.insert(buf, (" "):rep(indent + 2) .. table.concat(command._aliases, "|") .. ")") command:_zsh_arguments(buf, name, indent + 4) command:_zsh_complete_help(buf, cmds_buf, name, indent + 4) table.insert(buf, (" "):rep(indent + 4) .. ";;\n") end table.insert(buf, (" "):rep(indent) .. "esac") end function Parser:get_zsh_complete() self._basename = base_name(self._name) assert(self:_is_shell_safe()) local buf = {("#compdef %s\n"):format(self._basename)} local cmds_buf = {} table.insert(buf, "_" .. self._basename .. "() {") if #self._commands > 0 then table.insert(buf, " local context state state_descr line") table.insert(buf, " typeset -A opt_args\n") end self:_zsh_arguments(buf, self._basename, 2) self:_zsh_complete_help(buf, cmds_buf, self._basename, 2) table.insert(buf, "\n return 1") table.insert(buf, "}") local result = table.concat(buf, "\n") if #cmds_buf > 0 then result = result .. "\n" .. table.concat(cmds_buf, "\n") end return result .. "\n\n_" .. self._basename .. "\n" end local function fish_escape(string) return string:gsub("[\\']", "\\%0") end function Parser:_fish_get_cmd(buf, indent) if #self._commands == 0 then return end table.insert(buf, (" "):rep(indent) .. "set -e cmdline[1]") table.insert(buf, (" "):rep(indent) .. "for arg in $cmdline") table.insert(buf, (" "):rep(indent + 4) .. "switch $arg") for _, command in ipairs(self._commands) do table.insert(buf, (" "):rep(indent + 8) .. "case " .. table.concat(command._aliases, " ")) table.insert(buf, (" "):rep(indent + 12) .. "set cmd $cmd " .. command._name) command:_fish_get_cmd(buf, indent + 12) table.insert(buf, (" "):rep(indent + 12) .. "break") end table.insert(buf, (" "):rep(indent + 4) .. "end") table.insert(buf, (" "):rep(indent) .. "end") end function Parser:_fish_complete_help(buf, basename) local prefix = "complete -c " .. basename table.insert(buf, "") for _, command in ipairs(self._commands) do local aliases = table.concat(command._aliases, " ") local line if self._parent then line = ("%s -n '__fish_%s_using_command %s' -xa '%s'") :format(prefix, basename, self:_get_fullname(true), aliases) else line = ("%s -n '__fish_%s_using_command' -xa '%s'"):format(prefix, basename, aliases) end if command._description then line = ("%s -d '%s'"):format(line, fish_escape(get_short_description(command))) end table.insert(buf, line) end if self._is_help_command then local line = ("%s -n '__fish_%s_using_command %s' -xa '%s'") :format(prefix, basename, self:_get_fullname(true), self._parent:_get_commands()) table.insert(buf, line) end for _, option in ipairs(self._options) do local parts = {prefix} if self._parent then table.insert(parts, "-n '__fish_" .. basename .. "_seen_command " .. self:_get_fullname(true) .. "'") end for _, alias in ipairs(option._aliases) do if alias:match("^%-.$") then table.insert(parts, "-s " .. alias:sub(2)) elseif alias:match("^%-%-.+") then table.insert(parts, "-l " .. alias:sub(3)) end end if option._choices then table.insert(parts, "-xa '" .. table.concat(option._choices, " ") .. "'") elseif option._minargs > 0 then table.insert(parts, "-r") end if option._description then table.insert(parts, "-d '" .. fish_escape(get_short_description(option)) .. "'") end table.insert(buf, table.concat(parts, " ")) end for _, command in ipairs(self._commands) do command:_fish_complete_help(buf, basename) end end function Parser:get_fish_complete() self._basename = base_name(self._name) assert(self:_is_shell_safe()) local buf = {} if #self._commands > 0 then table.insert(buf, ([[ function __fish_%s_print_command set -l cmdline (commandline -poc) set -l cmd]]):format(self._basename)) self:_fish_get_cmd(buf, 4) table.insert(buf, ([[ echo "$cmd" end function __fish_%s_using_command test (__fish_%s_print_command) = "$argv" and return 0 or return 1 end function __fish_%s_seen_command string match -q "$argv*" (__fish_%s_print_command) and return 0 or return 1 end]]):format(self._basename, self._basename, self._basename, self._basename)) end self:_fish_complete_help(buf, self._basename) return table.concat(buf, "\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:check_choices(argument) if self.element._choices then for _, choice in ipairs(self.element._choices) do if argument == choice then return end end local choices_list = "'" .. table.concat(self.element._choices, "', '") .. "'" local is_option = getmetatable(self.element) == Option self:error("%s%s must be one of %s", is_option and "argument for " or "", self.name, choices_list) end end function ElementState:pass(argument) self:check_choices(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.7.1" setmetatable(argparse, {__call = function(_, ...) return Parser(default_cmdline[0]):add_help(true)(...) end}) return argparse