diff --git a/spec/actions_spec.lua b/spec/actions_spec.lua index 8f365e1..b750a66 100644 --- a/spec/actions_spec.lua +++ b/spec/actions_spec.lua @@ -3,8 +3,14 @@ getmetatable(Parser()).error = function(_, msg) error(msg) end describe("tests related to actions", function() it("calls actions for options", function() - local action1 = spy.new(function() end) - local action2 = spy.new(function() end) + local action1 = spy.new(function(_, _, arg) + assert.equal("nowhere", arg) + end) + local expected_args = {"Alice", "Bob"} + local action2 = spy.new(function(_, _, args) + assert.same(expected_args, args) + expected_args = {"Emma", "John"} + end) local parser = Parser() parser:option "-f" "--from" { @@ -16,8 +22,7 @@ describe("tests related to actions", function() args = 2 } - local args = parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"} - assert.same({from = "nowhere", pair = {{"Alice", "Bob"}, {"Emma", "John"}}}, args) + parser:parse{"-fnowhere", "--pair", "Alice", "Bob", "-p", "Emma", "John"} assert.spy(action1).called(1) assert.spy(action2).called(2) end) @@ -39,27 +44,9 @@ describe("tests related to actions", function() action = function(...) return action3(...) end } - local args = parser:parse{"-vv", "--quiet"} - assert.same({verbose = 2, quiet = true}, args) + parser:parse{"-vv", "--quiet"} assert.spy(action1).called(2) assert.spy(action2).called(1) assert.spy(action3).called(0) end) - - it("calls actions for commands", function() - local action = spy.new(function() end) - - local parser = Parser "name" - parser:flag "-v" "--verbose" { - count = "0-3" - } - local add = parser:command "add" { - action = function(...) return action(...) end - } - add:argument "something" - - local args = parser:parse{"add", "me"} - assert.same({add = true, verbose = 0, something = "me"}, args) - assert.spy(action).called(1) - end) end) diff --git a/spec/arguments_spec.lua b/spec/arguments_spec.lua index bc38eac..6185d8d 100644 --- a/spec/arguments_spec.lua +++ b/spec/arguments_spec.lua @@ -97,7 +97,7 @@ describe("tests related to positional arguments", function() local parser = Parser() parser:argument "foo" - assert.has_error(function() parser:parse{} end, "too few arguments") + assert.has_error(function() parser:parse{} end, "argument 'foo' is required") end) it("handles extra arguments with several arguments correctly", function() @@ -113,7 +113,7 @@ describe("tests related to positional arguments", function() parser:argument "foo1" parser:argument "foo2" - assert.has_error(function() parser:parse{"bar"} end, "too few arguments") + assert.has_error(function() parser:parse{"bar"} end, "argument 'foo2' is required") end) it("handles too few arguments with multi-argument correctly", function() @@ -121,7 +121,7 @@ describe("tests related to positional arguments", function() parser:argument "foo" { args = "+" } - assert.has_error(function() parser:parse{} end, "too few arguments") + assert.has_error(function() parser:parse{} end, "argument 'foo' is required") end) it("handles too many arguments with multi-argument correctly", function() @@ -137,7 +137,7 @@ describe("tests related to positional arguments", function() parser:argument "foo" { args = "2-4" } - assert.has_error(function() parser:parse{"foo"} end, "too few arguments") + assert.has_error(function() parser:parse{"foo"} end, "argument 'foo' requires at least 2 arguments") end) it("handles too many arguments with several multi-arguments correctly", function() @@ -159,7 +159,7 @@ describe("tests related to positional arguments", function() parser:argument "foo2" { args = "*" } - assert.has_error(function() parser:parse{} end, "too few arguments") + assert.has_error(function() parser:parse{} end, "argument 'foo1' is required") end) end) end) diff --git a/spec/default_spec.lua b/spec/default_spec.lua index 3e91c5e..40094ac 100644 --- a/spec/default_spec.lua +++ b/spec/default_spec.lua @@ -55,7 +55,7 @@ describe("tests related to default values", function() assert.same({output = "a.out"}, args) args = parser:parse{"--output", "foo.txt"} assert.same({output = "foo.txt"}, args) - assert.has_error(function() parser:parse{"-o"} end, "too few arguments") + assert.has_error(function() parser:parse{"-o"} end, "option '-o' requires an argument") end) it("handles option with default value for multi-argument option correctly", function() diff --git a/spec/integrity_spec.lua b/spec/integrity_spec.lua index c81abbf..d473ac9 100644 --- a/spec/integrity_spec.lua +++ b/spec/integrity_spec.lua @@ -20,7 +20,7 @@ describe("tests related to CLI behaviour #unsafe", function() assert.equal([[ Usage: ]]..script..[[ [-v] [-h] [] ... -Error: too few arguments +Error: argument 'input' is required ]], get_output("")) end) @@ -62,7 +62,7 @@ Did you mean 'install'? assert.equal([[ Usage: ]]..script..[[ install [-f ] [-h] [] -Error: too few arguments +Error: argument 'rock' is required ]], get_output("foo install")) end) diff --git a/spec/options_spec.lua b/spec/options_spec.lua index 9e0aed7..3ebfdff 100644 --- a/spec/options_spec.lua +++ b/spec/options_spec.lua @@ -258,7 +258,8 @@ describe("tests related to options", function() it("handles lack of required argument correctly", function() local parser = Parser() parser:option "-s" "--server" - assert.has_error(function() parser:parse{"--server"} end, "too few arguments") + assert.has_error(function() parser:parse{"--server"} end, "option '--server' requires an argument") + assert.has_error(function() parser:parse{"-s"} end, "option '-s' requires an argument") end) it("handles unknown options correctly", function() @@ -289,7 +290,7 @@ describe("tests related to options", function() count = 1, overwrite = false } - assert.has_error(function() parser:parse{"-qq"} end, "option '-q' must be used at most 1 time") + assert.has_error(function() parser:parse{"-qq"} end, "option '-q' must be used 1 time") end) it("handles too few invocations correctly", function() diff --git a/spec/pparse_spec.lua b/spec/pparse_spec.lua index a37d308..b72abd6 100644 --- a/spec/pparse_spec.lua +++ b/spec/pparse_spec.lua @@ -15,13 +15,13 @@ describe("tests related to :pparse()", function() parser:argument "foo" local ok, errmsg = parser:pparse{} assert.is_false(ok) - assert.equal("too few arguments", errmsg) + assert.equal("argument 'foo' is required", errmsg) end) - it("still raises an error if it is caused by misconfiguration", function() + it("rethrows errors from callbacks", function() local parser = Parser() parser:flag "--foo" - :action(error) - assert.has_error(function() parser:pparse{"--foo"} end) + :action(function() error("some error message") end) + assert.error_matches(function() parser:pparse{"--foo"} end, "some error message") end) end) diff --git a/src/argparse.lua b/src/argparse.lua index 08ac352..8c652c4 100644 --- a/src/argparse.lua +++ b/src/argparse.lua @@ -19,7 +19,6 @@ -- 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 @@ -35,61 +34,63 @@ 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 new_class(prototype, properties, parent) +local function class(prototype, properties, parent) -- Class is the metatable of its instances. - local class = {} - class.__index = class + local cl = {} + cl.__index = cl if parent then - class.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) + cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) else - class.__prototype = prototype + cl.__prototype = prototype end - local names = {} + 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] + -- Create setter methods and fill set of property names. + for _, property in ipairs(properties) do + local name, callback = property[1], property[2] - class[name] = function(self, value) - if not callback(self, value) then - self["_" .. name] = value + 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 - - names[name] = true - end - - function class.__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 -- If indexing class fails, fallback to its parent. @@ -104,7 +105,7 @@ local function new_class(prototype, properties, parent) return object(...) end - return setmetatable(class, class_metatable) + return setmetatable(cl, class_metatable) end local function typecheck(name, types, value) @@ -204,7 +205,7 @@ local add_help = {"add_help", function(self, value) end end} -local Parser = new_class({ +local Parser = class({ _arguments = {}, _options = {}, _commands = {}, @@ -223,7 +224,7 @@ local Parser = new_class({ add_help }) -local Command = new_class({ +local Command = class({ _aliases = {} }, { args = 3, @@ -235,11 +236,10 @@ local Command = new_class({ typechecked("help", "string"), typechecked("require_command", "boolean"), typechecked("handle_options", "boolean"), - typechecked("action", "function"), add_help }, Parser) -local Argument = new_class({ +local Argument = class({ _minargs = 1, _maxargs = 1, _mincount = 1, @@ -259,7 +259,7 @@ local Argument = new_class({ typechecked("argname", "string", "table") }) -local Option = new_class({ +local Option = class({ _aliases = {}, _mincount = 0, _overwrite = true @@ -322,22 +322,42 @@ function Argument:_get_usage() return usage end -function Argument:_get_type() +local actions = {} + +function actions.store_true(result, target) + result[target] = true +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) + table.insert(result[target], argument) + + if overwrite then + table.remove(result[target], 1) + end +end + +function Argument:_get_action() if self._maxcount == 1 then if self._maxargs == 0 then - return "flag" - elseif self._maxargs == 1 and (self._minargs == 1 or self._mincount == 1) then - return "arg" + return self._action or actions.store_true, nil else - return "multiarg" + return self._action or actions.store, nil end else if self._maxargs == 0 then - return "counter" - elseif self._maxargs == 1 and self._minargs == 1 then - return "multicount" + return self._action or actions.count, 0 else - return "twodimensional" + return self._action or actions.append, {} end end end @@ -411,6 +431,10 @@ function Option:_get_usage() return usage end +function Argument:_get_default_target() + return self._name +end + function Option:_get_default_target() local res @@ -635,19 +659,21 @@ local function get_tip(context, wrong_name) local possible_names = {} for name in pairs(context) do - for i=1, #name do - possible_name = name:sub(1, i-1) .. name:sub(i+1) + 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] = {} + if not context_pool[possible_name] then + context_pool[possible_name] = {} + end + + table.insert(context_pool[possible_name], name) end - - table.insert(context_pool[possible_name], name) end end - for i=1, #wrong_name+1 do - possible_name = wrong_name:sub(1, i-1) .. wrong_name:sub(i+1) + 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 @@ -659,6 +685,7 @@ local function get_tip(context, wrong_name) end local first = next(possible_names) + if first then if next(possible_names, first) then local possible_names_arr = {} @@ -677,350 +704,360 @@ local function get_tip(context, wrong_name) end end -local function plural(x) - if x == 1 then - return "" - end +local ElementState = class({ + invocations = 0 +}) - return "s" +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 --- Compatibility with strict.lua and other checkers: -local default_cmdline = rawget(_G, "arg") or {} +function ElementState:error(fmt, ...) + self.state:error(fmt, ...) +end -function Parser:_parse(args, errhandler) - args = args or default_cmdline - local parser - local charset - local options = {} - local arguments = {} - local commands - local option_mutexes = {} - local used_mutexes = {} - local opt_context = {} - local com_context - local result = {} - local invocations = {} - local passed = {} - local cur_option - local cur_arg_i = 1 - local cur_arg - local targets = {} - local handle_options = true +function ElementState:convert(argument) + local converter = self.element._convert - local function error_(fmt, ...) - return errhandler(parser, fmt:format(...)) - end + if converter then + local ok, err - local function assert_(assertion, ...) - return assertion or error_(...) - end - - local function convert(element, data) - if element._convert then - local ok, err - - if type(element._convert) == "function" then - ok, err = element._convert(data) - else - ok = element._convert[data] - end - - assert_(ok ~= nil, "%s", err or "malformed argument '" .. data .. "'") - data = ok - end - - return data - end - - local invoke, pass, close - - function invoke(element) - local overwrite = false - - if invocations[element] == element._maxcount then - if element._overwrite then - overwrite = true - else - error_("option '%s' must be used at most %d time%s", element._name, element._maxcount, plural(element._maxcount)) - end + if type(converter) == "function" then + ok, err = converter(argument) else - invocations[element] = invocations[element]+1 + ok = converter[argument] end - passed[element] = 0 - local type_ = element:_get_type() - local target = targets[element] - - if type_ == "flag" then - result[target] = true - elseif type_ == "multiarg" then - result[target] = {} - elseif type_ == "counter" then - if not overwrite then - result[target] = result[target]+1 - end - elseif type_ == "multicount" then - if overwrite then - table.remove(result[target], 1) - end - elseif type_ == "twodimensional" then - table.insert(result[target], {}) - - if overwrite then - table.remove(result[target], 1) - end + if ok == nil then + self:error(err and "%s" or "malformed argument '%s'", err or argument) end - if element._maxargs == 0 then - close(element) - end + argument = ok end - function pass(element, data) - passed[element] = passed[element]+1 - data = convert(element, data) - local type_ = element:_get_type() - local target = targets[element] + return argument +end - if type_ == "arg" then - result[target] = data - elseif type_ == "multiarg" or type_ == "multicount" then - table.insert(result[target], data) - elseif type_ == "twodimensional" then - table.insert(result[target][#result[target]], data) - end +function ElementState:default(mode) + return self.element._defmode:find(mode) and self.element._default +end - if passed[element] == element._maxargs then - close(element) - 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 function complete_invocation(element) - while passed[element] < element._minargs do - pass(element, element._default) - end - end + local number = is_max and max or min + return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") +end - function close(element) - if passed[element] < element._minargs then - if element._default and element._defmode:find "a" then - complete_invocation(element) - else - error_("too few arguments") - end +function ElementState:invoke(alias) + self.open = true + self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) + self.overwrite = false + + if self.invocations >= self.element._maxcount then + if self.element._overwrite then + self.overwrite = true else - if element == cur_option then - cur_option = nil - elseif element == cur_arg then - cur_arg_i = cur_arg_i+1 - cur_arg = arguments[cur_arg_i] - end + self:error("%s must be used %s", self.name, bound("time", self.element._mincount, self.element._maxcount, true)) end + else + self.invocations = self.invocations + 1 end - local function switch(p) - parser = p + self.args = {} - for _, option in ipairs(parser._options) do - table.insert(options, option) + if self.element._maxargs <= 0 then + self:close() + end - for _, alias in ipairs(option._aliases) do - opt_context[alias] = option - end + return self.open +end - local type_ = option:_get_type() - targets[option] = option._target or option:_get_default_target() +function ElementState:pass(argument) + argument = self:convert(argument) + table.insert(self.args, argument) - if type_ == "counter" then - result[targets[option]] = 0 - elseif type_ == "multicount" or type_ == "twodimensional" then - result[targets[option]] = {} - end + if #self.args >= self.element._maxargs then + self:close() + end - invocations[option] = 0 - end + return self.open +end - for _, mutex in ipairs(parser._mutexes) do - for _, option in ipairs(mutex) do - if not option_mutexes[option] then - option_mutexes[option] = {mutex} - else - table.insert(option_mutexes[option], mutex) +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("%s is required", 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 - for _, argument in ipairs(parser._arguments) do - table.insert(arguments, argument) - invocations[argument] = 0 - targets[argument] = argument._target or argument._name - invoke(argument) + local args = self.args + + if self.element._maxargs <= 1 then + args = args[1] end - handle_options = parser._handle_options - cur_arg = arguments[cur_arg_i] - commands = parser._commands - com_context = {} + if self.element._maxargs == 1 and self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then + args = self.args + end - for _, command in ipairs(commands) do - targets[command] = command._target or command._name + self.action(self.result, self.target, args, self.overwrite) + end +end - for _, alias in ipairs(command._aliases) do - com_context[alias] = command +local ParseState = class({ + result = {}, + options = {}, + arguments = {}, + argument_i = 1, + element_to_mutexes = {}, + mutex_to_used_option = {} +}) + +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 + + 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 _, option in ipairs(mutex) do + if not self.element_to_mutexes[option] then + self.element_to_mutexes[option] = {} end + + table.insert(self.element_to_mutexes[option], mutex) end end - local function get_option(name) - return assert_(opt_context[name], "unknown option '%s'%s", name, get_tip(opt_context, name)) + for _, argument in ipairs(parser._arguments) do + argument = ElementState(self, argument) + table.insert(self.arguments, argument) + argument:invoke() end - local function do_action(element) - if element._action then - element._action() + 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 - local function handle_argument(data) - if cur_option then - pass(cur_option, data) - elseif cur_arg then - pass(cur_arg, data) +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 - local com = com_context[data] + self:error("too many arguments") + end + else + return command + end +end - if not com then - if #commands > 0 then - error_("unknown command '%s'%s", data, get_tip(com_context, data)) - else - error_("too many arguments") +function ParseState:invoke(option, name) + self:close() + + if self.element_to_mutexes[option.element] then + for _, mutex in ipairs(self.element_to_mutexes[option.element]) do + local used_option = self.mutex_to_used_option[mutex] + + if used_option and used_option ~= option then + self:error("option '%s' can not be used together with %s", name, used_option.name) + else + self.mutex_to_used_option[mutex] = option + end + end + end + + if option:invoke(name) 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 + 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 + 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 + local name = option.name or ("option '%s'"):format(option.element._name) + + if option.invocations == 0 then + if option:default("u") then + option:invoke(name) + 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(name) + option:close() end else - result[targets[com]] = true - do_action(com) - switch(com) + self:error("%s must be used %s", name, bound("time", mincount, option.element._maxcount)) end end end +end - local function handle_option(data) - if cur_option then - close(cur_option) - end +function ParseState:parse(args) + for _, arg in ipairs(args) do + local plain = true - cur_option = opt_context[data] + if self.handle_options then + local first = arg:sub(1, 1) - if option_mutexes[cur_option] then - for _, mutex in ipairs(option_mutexes[cur_option]) do - if used_mutexes[mutex] and used_mutexes[mutex] ~= cur_option then - error_("option '%s' can not be used together with option '%s'", data, used_mutexes[mutex]._name) - else - used_mutexes[mutex] = cur_option - end - end - end + if self.charset[first] then + if #arg > 1 then + plain = false - do_action(cur_option) - invoke(cur_option) - end - - local function mainloop() - - for _, data in ipairs(args) do - local plain = true - local first, name, option - - if handle_options then - first = data:sub(1, 1) - if charset[first] then - if #data > 1 then - plain = false - if data:sub(2, 2) == first then - if #data == 2 then - if cur_option then - close(cur_option) - end - - handle_options = false - else - local equal = data:find "=" - if equal then - name = data:sub(1, equal-1) - option = get_option(name) - assert_(option._maxargs > 0, "option '%s' does not take arguments", name) - - handle_option(data:sub(1, equal-1)) - handle_argument(data:sub(equal+1)) - else - get_option(data) - handle_option(data) - end - end + if arg:sub(2, 2) == first then + if #arg == 2 then + self:close() + self.handle_options = false else - for i = 2, #data do - name = first .. data:sub(i, i) - option = get_option(name) - handle_option(name) + local equals = arg:find "=" + if equals then + local name = arg:sub(1, equals - 1) + local option = self:get_option(name) - if i ~= #data and option._minargs > 0 then - handle_argument(data:sub(i+1)) - break + 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._minargs > 0 and option.element._maxargs > 0 then + self:pass(arg:sub(i + 1)) + break end end end end end + end - if plain then - handle_argument(data) - end + if plain then + self:pass(arg) end end - switch(self) - charset = parser:_update_charset() - mainloop() - - if cur_option then - close(cur_option) - end - - while cur_arg do - if passed[cur_arg] == 0 and cur_arg._default and cur_arg._defmode:find "u" then - complete_invocation(cur_arg) - else - close(cur_arg) - end - end - - if parser._require_command and #commands > 0 then - error_("a command is required") - end - - for _, option in ipairs(options) do - if invocations[option] == 0 then - if option._default and option._defmode:find "u" then - invoke(option) - complete_invocation(option) - close(option) - end - end - - if invocations[option] < option._mincount then - if option._default and option._defmode:find "a" then - while invocations[option] < option._mincount do - invoke(option) - close(option) - end - else - error_("option '%s' must be used at least %d time%s", option._name, option._mincount, plural(option._mincount)) - end - end - end - - return result + self:finalize() + return self.result end function Parser:error(msg) @@ -1028,24 +1065,37 @@ function Parser:error(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, Parser.error) + 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 errmsg - local ok, result = pcall(function() + local parse_error + + local ok, result = xpcall(function() return self:_parse(args, function(_, err) - errmsg = err - return error() + parse_error = err + error(err, 0) end) - end) + end, xpcall_error_handler) if ok then return true, result + elseif not parse_error then + error(result, 0) else - assert(errmsg, result) - return false, errmsg + return false, parse_error end end